<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>개발자가 들려주는 IT 이야기</title>
    <link>https://code-tales.tistory.com/</link>
    <description>안드로이드, 리액트 등의 개발 노하우와 최신 AI 기술을 다루는 기술 블로그입니다. 

실무 중심의 경험을 바탕으로 마주한 문제와 해결 과정을 체계적으로 기록하며, 개발자에게 실질적으로 도움 되는 프로그래밍 팁과 인사이트를 쉽고 명확하게 공유하고자 합니다.</description>
    <language>ko</language>
    <pubDate>Sun, 12 Jul 2026 08:09:32 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>코딩덕</managingEditor>
    <image>
      <title>개발자가 들려주는 IT 이야기</title>
      <url>https://tistory1.daumcdn.net/tistory/6494358/attach/f8d30b2bf588403cbf084da81ef54489</url>
      <link>https://code-tales.tistory.com</link>
    </image>
    <item>
      <title>[Claude Code 입문 가이드] 설치부터 반드시 알아야 할 기본 기능 6가지 #1</title>
      <link>https://code-tales.tistory.com/70</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 개요&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`Claude Code`는 Anthropic이 만든 터미널 기반 AI 코딩 에이전트입니다. ChatGPT나 Cursor처럼 에디터에 붙어서 코드 한 줄을 완성해 주는 수준이 아니라, 프로젝트 전체를 이해하고 파일을 읽고 쓰고 실행까지 자율적으로 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 개발을 위해 `Claude Code`를 처음 접할 때 반드시 알아야 할 기본 기능을 단계별로 정리했습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. Claude Code 기본 기능 정리&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-1. 설치 및 계정 연결&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치는 운영체제에 따라 한 줄 명령어로 끝납니다.&lt;/p&gt;
&lt;pre id=&quot;code_1775110030154&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# macOS / Linux
curl -fsSL https://claude.ai/install.sh | bash&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1775110074732&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Window PowerShell
irm https://claude.ai/install.ps1 | iex&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`Homebrew` 또는 `npm` 설치도 가능하지만 공식 인스톨러 사용을 권장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;자동으로 업데이트를 지원&lt;/b&gt;하기 때문입니다. 설치 후 `claude --version`으로 정상 설치를 확인할 수 있습니다.&lt;br /&gt;&lt;br /&gt;터미널에서 `claude`를 입력하면 계정 연결 화면이 나타납니다.&lt;br /&gt;&lt;b&gt;Claude Pro&lt;/b&gt; 이상 구독이 필요하며 사용량이 많다면 &lt;b&gt;Max 플랜($200/월)&lt;/b&gt;이 가성비가 가장 좋습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-2. 모델 선택&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클로드 코드 실행 후, `/model` 명령어로 AI 모델을 선택할 수 있고, 현재 세 가지 옵션이 있습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 82.3256%; height: 72px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style14&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 14.2635%; height: 19px;&quot;&gt;모델&lt;/td&gt;
&lt;td style=&quot;width: 22.5194%; height: 19px;&quot;&gt;특징&lt;/td&gt;
&lt;td style=&quot;width: 45.5426%; height: 19px;&quot;&gt;추천 상황&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 14.2635%; height: 19px;&quot;&gt;Haiku&lt;/td&gt;
&lt;td style=&quot;width: 22.5194%; height: 19px;&quot;&gt;- 가장 빠른 응답&lt;/td&gt;
&lt;td style=&quot;width: 45.5426%; height: 19px;&quot;&gt;- 간단한 질문, 파일 찾기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 14.2635%; height: 17px;&quot;&gt;Sonnet&lt;/td&gt;
&lt;td style=&quot;width: 22.5194%; height: 17px;&quot;&gt;- 올라운더&lt;br /&gt;- 속도 및 품질 균형&lt;/td&gt;
&lt;td style=&quot;width: 45.5426%; height: 17px;&quot;&gt;- 일반적인 개발 작업&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 14.2635%; height: 17px;&quot;&gt;Opus&lt;/td&gt;
&lt;td style=&quot;width: 22.5194%; height: 17px;&quot;&gt;- 최강 추론 능력&lt;br /&gt;- 토큰 소모 큼&lt;/td&gt;
&lt;td style=&quot;width: 45.5426%; height: 17px;&quot;&gt;- 복잡한 설계&lt;br /&gt;- 대규모 코드 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 업데이트로 Opus 모델은 &lt;b&gt;1M 토큰 컨텍스트 윈도우&lt;/b&gt;를 지원합니다.&lt;br /&gt;즉, 대화 내용을 더 많이 기억하고 오랫동안 작업할 수 있습니다. &lt;br /&gt;&lt;b&gt;Max 플랜&lt;/b&gt;이라면 &lt;b&gt;Opus 4.6&lt;/b&gt;을 기본으로 사용하는 것을 추천합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2576&quot; data-origin-height=&quot;552&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yfbg4/dJMcaiivWM8/MZlxAeqeLCofUgl0hkxUC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yfbg4/dJMcaiivWM8/MZlxAeqeLCofUgl0hkxUC1/img.png&quot; data-alt=&quot;/model 명령어 사용 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yfbg4/dJMcaiivWM8/MZlxAeqeLCofUgl0hkxUC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fyfbg4%2FdJMcaiivWM8%2FMZlxAeqeLCofUgl0hkxUC1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2576&quot; height=&quot;552&quot; data-origin-width=&quot;2576&quot; data-origin-height=&quot;552&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;/model 명령어 사용 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-3. 모드 전환&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`Shift+Tab`을 누르면 모드가 전환됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Accept edits on:&lt;/b&gt; 코드 변경을 자동으로 수락&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Plan mode:&lt;/b&gt; 계획만 세우고 코드는 변경하지 않음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Default mode:&lt;/b&gt; 파일 diff를 확인하며 진행&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`claude --dangerously-skip-permissions`로 실행하면 모든 권한 확인을 스킵할 수 있습니다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;편리하긴 하지만 프롬프트 인젝션 등의 위험이 있으므로 주의해야 합니다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-4. Plan Mode&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`Claude Code`를 처음에 사용할 때 추천하는 방식은 항상 `Plan Mode`부터 시작하는 것입니다.&lt;br /&gt;&lt;br /&gt;계획 없이 바로 코드 생성을 요청하면 엉뚱한 방향으로 달려가고, 수정하는 데 처음부터 제대로 계획하는 것보다 훨씬 오래 걸립니다. Plan Mode에서는 코드를 전혀 변경하지 않고, AI가 인터뷰어처럼 질문을 던지며 요구사항을 구체화합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1198&quot; data-origin-height=&quot;542&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xvmyN/dJMcag55flH/QbGOVUhtvsOYbJzktxbPM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xvmyN/dJMcag55flH/QbGOVUhtvsOYbJzktxbPM1/img.png&quot; data-alt=&quot;Claude Code에서 Plan Mode 사용중인 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xvmyN/dJMcag55flH/QbGOVUhtvsOYbJzktxbPM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxvmyN%2FdJMcag55flH%2FQbGOVUhtvsOYbJzktxbPM1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;Claude Code PlanMode&quot; loading=&quot;lazy&quot; width=&quot;550&quot; height=&quot;249&quot; data-origin-width=&quot;1198&quot; data-origin-height=&quot;542&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Claude Code에서 Plan Mode 사용중인 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;큰 프로젝트에서는 전체를 한 번에 계획하지 말고, 기능 단위로 &lt;b&gt;Plan Mode&lt;/b&gt;를 분리해서 사용하는 것도 좋습니다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-5. 계획 실행&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`Yes, and bypass permissions`를 선택하면 계획이 실행됩니다.&lt;br /&gt;이전 대화 컨텍스트는 클리어되지만, 계획은 `. claude`폴더에 마크다운으로 저장되어 유지됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-6. Memory와 CLAUDE.md&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Code는 `. claude`폴더를 통해 프로젝트 수준의 기억을 유지합니다.&lt;br /&gt;`/init`명령어로 `CLAUDE.md`파일을 생성할 수 있으며, 이 파일은 매 프롬프트에 자동으로 주입되는 &lt;b&gt;시스템 프롬프트&lt;/b&gt; 역할을 합니다.&lt;br /&gt;&lt;br /&gt;아키텍처 컨벤션, 라이브러리 규칙, 코드 스타일 등을 기록해두면 매번 설명할 필요가 없습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;단, MIT 연구에서 시스템 프롬프트 파일이 오히려 품질을 저하시킬 수 있다는 결과도 있습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;토큰을 소모하고 모델의 집중도를 분산시킬 수 있기 때문입니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;꼭 필요한 핵심 규칙만 간결하게 담는 방식&lt;/b&gt;으로 활용하세요.&lt;/span&gt;&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-7. 콘텍스트 관리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 대화 기록이 곧 콘텍스트입니다. 대화가 길어질수록 환각이 증가하고 응답 품질이 떨어집니다.&lt;br /&gt;LLM에는 'Lost in the middle'이라는 현상이 있어, 긴 프롬프트의 중간 정보가 소실되는 경향이 있습니다.&lt;br /&gt;&lt;br /&gt;`/context` 명령어로 토큰 사용량을 시각화 할 수 있는데 사용량이 70% 이상 도달했다면 정리가 필요합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1534&quot; data-origin-height=&quot;818&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bagfHP/dJMcaiv1wZ8/V7pI7MyTTyVIhutQ5NDBJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bagfHP/dJMcaiv1wZ8/V7pI7MyTTyVIhutQ5NDBJ0/img.png&quot; data-alt=&quot;/context 명령어 사용 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bagfHP/dJMcaiv1wZ8/V7pI7MyTTyVIhutQ5NDBJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbagfHP%2FdJMcaiv1wZ8%2FV7pI7MyTTyVIhutQ5NDBJ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;550&quot; height=&quot;293&quot; data-origin-width=&quot;1534&quot; data-origin-height=&quot;818&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;/context 명령어 사용 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 82.0921%; height: 76px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style14&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 18.9535%; height: 19px;&quot;&gt;명령어&lt;/td&gt;
&lt;td style=&quot;width: 66.4965%; height: 19px;&quot;&gt;동작&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 18.9535%; height: 19px;&quot;&gt;`/clear`&lt;/td&gt;
&lt;td style=&quot;width: 66.4965%; height: 19px;&quot;&gt;- 대화 기록 완전 초기화 (계획 및 파일은 유지)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 38px;&quot;&gt;
&lt;td style=&quot;width: 18.9535%; height: 38px;&quot;&gt;`/compact`&lt;/td&gt;
&lt;td style=&quot;width: 66.4965%; height: 38px;&quot;&gt;- 핵심 정보를 보존하면서 요약&lt;br /&gt;- 어떻게 요약하는지 컨트롤 할 수 없어서 비추천&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 결론&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 `Claude Code`의 기본 기능에 대해 살펴봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클로드 코드는 단순한 코드 자동완성이 아니라 프로젝트를 이해하고 자율적으로 실행하는 AI 에이전트입니다.&lt;br /&gt;제대로 활용하려면 몇 가지 원칙을 지키는 게 좋습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;항상 &lt;b&gt;Plan Mode&lt;/b&gt;에서 시작 -&amp;gt; 계획 없이 코드 생성부터 시키지 않기&lt;/li&gt;
&lt;li&gt;&lt;b&gt;컨텍스트 70%&lt;/b&gt;에서 정리 -&amp;gt; `/clear` 또는 `/compact` 활용&lt;/li&gt;
&lt;li&gt;생성된 코드는 반드시 리뷰 -&amp;gt; 규모자 커질수록 이상한 코드가 섞일 수 있음. Git 활용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Git&lt;/b&gt;으로 커밋 및 브랜치 관리 -&amp;gt; AI가 망칠 때 롤백할 수 있는 안전망 확보&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자의 역할이 코드 작성자에서 &lt;b&gt;AI 에이전트 오케스트레이터&lt;/b&gt;로 바뀌고 있습니다.&lt;br /&gt;다만 기술적 기초 없이 오직 AI에만 의존하면 바이브 코더에 불과합니다.&lt;br /&gt;Claude Code를 잘 다루는 것도 결국 기술적 이해도에서 시작한다고 생각합니다.&lt;br /&gt;&lt;br /&gt;다음 포스팅에서는 MCP, Skills에 대하여 알아보겠습니다.&lt;/p&gt;</description>
      <category>AI Tool/Claude Code</category>
      <category>claude code</category>
      <category>클로드 코드</category>
      <category>클로드 코드 기초</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/70</guid>
      <comments>https://code-tales.tistory.com/70#entry70comment</comments>
      <pubDate>Fri, 3 Apr 2026 08:50:22 +0900</pubDate>
    </item>
    <item>
      <title>Next.js 다국어 라우팅 가이드 App Router + next-intl 설정 방법 정리 (1편)</title>
      <link>https://code-tales.tistory.com/69</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 개요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글로벌 웹 서비스를 구축하면서 &lt;b&gt;다국어 지원(internationalization, i18n)&lt;/b&gt; 기능을 직접 구현했던 경험을 정리해 공유합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 &lt;b&gt;Next.js 13 이상 버전의 App Router 환경&lt;/b&gt;에서&amp;nbsp; `next-intl` 라이브러리를 사용해 &lt;b&gt;언어별 URL 라우팅, 자동 리다이렉션, 번역 메시지 구성&lt;/b&gt; 등 다국어 라우팅을 설정하는 방법에 대해 단계별로 설명합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;30.03.2025 14_05.gif&quot; data-origin-width=&quot;2514&quot; data-origin-height=&quot;1718&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccmghc/btsM23nH26M/KM3hUa3zkOcv2rL4XltKUK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccmghc/btsM23nH26M/KM3hUa3zkOcv2rL4XltKUK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccmghc/btsM23nH26M/KM3hUa3zkOcv2rL4XltKUK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/ccmghc/btsM23nH26M/KM3hUa3zkOcv2rL4XltKUK/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;다국어 전환 모습&quot; loading=&quot;lazy&quot; width=&quot;550&quot; height=&quot;376&quot; data-filename=&quot;30.03.2025 14_05.gif&quot; data-origin-width=&quot;2514&quot; data-origin-height=&quot;1718&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;✅ 이번 글에서는 `next-intl` 설정만 다루고, 다음 글(2편)에서는 실제 화면에서 다국어 메시지 렌더링, 언어 전환 UI를 구현합니다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. next-intl을 활용한 다국어 라우팅 설정&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2-1. next-intl 라이브러리 설치&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 프로젝트에 `next-intl` 패키지를 설치합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1743308595423&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm install next-intl&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;React 기반의 Next.js App Router 구조&lt;/b&gt;와 잘 통합되어 있어서, 복잡한 설정 없이도 다국어 지원을 쉽게 구현할 수 있도록 도와줍니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2-2. 프로젝트 구조 설정&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`next-intl` 공식 문서에서도 권장하는 구조입니다. 이대로 설정하면 유지보수 및 확장에 유리합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1743308653557&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;project-root/
├── app/
│   ├── layout.tsx
│   ├── [locale]/
│   │   ├── layout.tsx
│   │   └── page.tsx
├── i18n/
│   ├── routing.ts
│   ├── request.ts
│   └── navigation.ts
├── messages/
│   ├── en.json
│   └── ko.json
├── middleware.ts
├── next.config.js&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나씩 구현해 보겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2-3. 지원 언어 및 기본 설정&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지원할 언어와 기본 언어를 `i18n/routing.ts`에서 정의합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1743308706097&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// i18n/routing.ts
import { defineRouting } from 'next-intl/routing';

export const routing = defineRouting({
  locales: ['en', 'ko'],	// 지원할 언어
  defaultLocale: 'en',		// 기본 언어
});&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2-4. 언어 접두어 자동 리다이렉션 설정&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;URL에 언어 접두어가 없는 경우, `/ko`, `/en` 등으로 자동 리다이렉션 되도록 설정해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;middleware를 활용하면 되고, Api 경로 또는 정적 리소스 경로는 제외되도록 했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1743308927728&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// middleware.ts
import createMiddleware from 'next-intl/middleware';
import { routing } from './i18n/routing';

// 언어 접두어 없는 경로 &amp;rarr; 자동 리디렉션 처리
export default createMiddleware(routing);

export const config = {
  // 다음 경로는 제외:
  // - /api, /trpc, /_next, /_vercel
  // - 정적 리소스 (.js, .css, .ico 등)
  matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)',
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;예시 동작&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 51px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;&lt;b&gt;요청 경로&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;&lt;b&gt;리다이렉션 결과&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;/&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;/ko&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;/about&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;/ko/about&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;/favicon.ico, /api/data 등 정적 리소스&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;그대로 유지&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2-5. 언어별 메시지 분리&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언어별 번역 메시지는 JSON 파일로 분리해 관리합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;  실제 운영에서는 구글 스프레드 시트 등으로 연동하여 JSON 파일을 자동 생성하는 방식이 유용합니다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2-6. 서버 측 메시지 로딩 설정&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청된 언어에 맞는 메시지를 동적으로 import 합니다.&lt;br /&gt;지원하지 않는 언어가 들어오면 `defaultLocale`로 fallback 처리됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1743309077933&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// i18n/request.ts

import { getRequestConfig } from 'next-intl/server';
import { hasLocale } from 'next-intl';
import { routing } from './routing';

export default getRequestConfig(async ({ requestLocale }) =&amp;gt; {
  // Typically corresponds to the `[locale]` segment
  const requested = await requestLocale;
  const locale = hasLocale(routing.locales, requested) ? requested : routing.defaultLocale;

  return {
    locale,
    messages: (await import(`@/messages/${locale}.json`)).default,
  };
});&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2-7. 라우팅 헬퍼 구성&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NextJS에서 기본 `Link`나 `useRouter`를 사용할 경우 언어 접두어를 자동 처리하지 않기 때문에, `next-intl`에서 제공하는 라우팅 헬퍼로 wrapping 해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1743309321578&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// i18n/navigation.ts
import { createNavigation } from 'next-intl/navigation';
import { routing } from './routing';

export const { Link, redirect, usePathname, useRouter, getPathname } = createNavigation(routing);&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 전체 구성 요약&lt;/b&gt;&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 120px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;&lt;b&gt;구성 요소&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;i18n/routing.ts&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;지원 언어 및 기본 언어 정의&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;i18n/request.ts&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;서버에서 언어 메시지 로딩&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;i18n/navigation.ts&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;NextJs의 라우팅 관련 래핑 함수 만들기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;messages/*.json&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;언어별 번역 메시지 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;middleware.ts&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;자동 리다이렉션 설정 (언어 감지 및 URL 리다이렉션)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. 결론&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js App Router 환경에서 `next-intl`을 활용한 다국어 설정은 복잡해 보일 수 있지만, 구조를 잘 정리하면 확장성과 유지보수성이 매우 높아집니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SEO에 유리한 URL 기반 언어 구조&lt;/li&gt;
&lt;li&gt;서버 사이드에서 안전하게 메시지 관리&lt;/li&gt;
&lt;li&gt;사용자의 언어 설정에 따라 자동 라우팅 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 &lt;b&gt;설정 중심의 내용을 다뤘으며,&lt;/b&gt;&lt;br /&gt;다음 글(2편)에서는 실제 다국어 텍스트를 화면에 렌더링 하고, 언어 전환 드롭다운을 구현하겠습니다.&lt;br /&gt;&lt;br /&gt;  이 글의 예제 프로젝트는 아래 GitHub 저장소에서 확인하실 수 있습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1743311568523&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - taein8935/next-intl-demo&quot; data-og-description=&quot;Contribute to taein8935/next-intl-demo development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/taein8935/next-intl-demo&quot; data-og-url=&quot;https://github.com/taein8935/next-intl-demo&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bkoPgo/hyYxPrW6tw/Ns41ibs9xgdTXBkGMJiKXk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bbXGr3/hyYvniQWXE/3PMos7zk6ZGYjKWVJHhSm0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/taein8935/next-intl-demo&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/taein8935/next-intl-demo&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bkoPgo/hyYxPrW6tw/Ns41ibs9xgdTXBkGMJiKXk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bbXGr3/hyYvniQWXE/3PMos7zk6ZGYjKWVJHhSm0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - taein8935/next-intl-demo&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to taein8935/next-intl-demo development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>리액트/NextJs</category>
      <category>I18N</category>
      <category>internationalization</category>
      <category>next.js</category>
      <category>react</category>
      <category>다국어</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/69</guid>
      <comments>https://code-tales.tistory.com/69#entry69comment</comments>
      <pubDate>Mon, 31 Mar 2025 07:35:43 +0900</pubDate>
    </item>
    <item>
      <title>[Github 코드 리뷰 자동화] Gemini Code Assist 앱 설치부터 AI 리뷰 활용까지</title>
      <link>https://code-tales.tistory.com/68</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 개요&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 리뷰는 시간과 리소스가 부족한 개발팀에겐 부담으로 작용하기도 합니다. 이를 위한 대안으로 AI Code Review를 도입할 수 있는데요. 구글이 새롭게 공개한 `Gemini Code Assists`는 Github Pull Request에 AI 기반의 요약과 코드 리뷰를 자동으로 추가해 줍니다.&lt;br /&gt;&lt;br /&gt;이번 글에서는 &lt;b&gt;Github에 Gemini Code Assist 앱을 설치&lt;/b&gt;하고, 실제로 코드 리뷰에 어떻게 활용되는지 다뤄보겠습니다.&lt;br /&gt;&lt;br /&gt;특히,&lt;b&gt; AI 리뷰 커스터마이징도 가능&lt;/b&gt;하므로 팀의 코딩 컨벤션 등을 적용해서 활용할 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Github의 Gemini Code Assist 앱 장점&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;무료&lt;/b&gt;로 사용 가능&lt;/li&gt;
&lt;li&gt;PR 작성 시, `PR 요약` 및 `리뷰 코멘트`가 자동으로 달림&lt;/li&gt;
&lt;li&gt;`.gemini/styleguide.me` 파일을 활용하면 커스터마이징 가능&lt;/li&gt;
&lt;li&gt;코드 품질 확보와 리뷰 리소스 절감을 동시에 기대할 수 있음&lt;/li&gt;
&lt;li&gt;`1인 개발자`도 AI한테 리뷰를 받아서, 생각하지 못했던 부분을 미리 확인하여 버그를 방지할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. Github에서 Gemini Code Assists 설치 및 설정&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2-1. 앱 설치하기&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;a href=&quot;https://github.com/apps/gemini-code-assist&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Gemini Code Assist Github App&lt;/a&gt;&lt;/b&gt; 페이지로 이동 후 &lt;b&gt;Install&lt;/b&gt; 버튼 클릭&lt;/li&gt;
&lt;li&gt;리뷰를 적용할 Github 조직 또는 개인 계정 선택&lt;/li&gt;
&lt;li&gt;설치할 저장소 범위 선택 (모든 저장소 또는 특정 저장소)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1423&quot; data-origin-height=&quot;817&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dZc5Wa/btsMT5mnt7o/TXZ5klwIbSk0UfnAky2nnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dZc5Wa/btsMT5mnt7o/TXZ5klwIbSk0UfnAky2nnK/img.png&quot; data-alt=&quot;Gemini Code Assist 앱 설치 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dZc5Wa/btsMT5mnt7o/TXZ5klwIbSk0UfnAky2nnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdZc5Wa%2FbtsMT5mnt7o%2FTXZ5klwIbSk0UfnAky2nnK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;Gemini Code Assist 앱 설치 화면&quot; loading=&quot;lazy&quot; width=&quot;550&quot; height=&quot;817&quot; data-origin-width=&quot;1423&quot; data-origin-height=&quot;817&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Gemini Code Assist 앱 설치 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2-2. 설치 후 동작 방식&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gemini Code Assist는 `PR 생성 시점` 또는 `코멘트 기반 트리거`에 의해 자동으로 리뷰를 생성합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드 변경사항을 분석하여 문제점, 개선 포인트, 스타일 위반 등을 자동으로 코멘트&lt;/li&gt;
&lt;li&gt;&lt;b&gt;리뷰 범위와 스타일&lt;/b&gt;은 기본 설정 외에도 `.gemini/styleguide.me` 파일로 커스터마이징 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1111&quot; data-origin-height=&quot;1393&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mvZco/btsMT2pFHnx/DXyYhvariZ9fLoxJrtKLpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mvZco/btsMT2pFHnx/DXyYhvariZ9fLoxJrtKLpK/img.png&quot; data-alt=&quot;Gemini Code Assist가 코드 리뷰를 해 준 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mvZco/btsMT2pFHnx/DXyYhvariZ9fLoxJrtKLpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmvZco%2FbtsMT2pFHnx%2FDXyYhvariZ9fLoxJrtKLpK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;Gemini Code Assist가 코드 리뷰를 해 준 모습&quot; loading=&quot;lazy&quot; width=&quot;550&quot; height=&quot;690&quot; data-origin-width=&quot;1111&quot; data-origin-height=&quot;1393&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Gemini Code Assist가 코드 리뷰를 해 준 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. AI 코드 리뷰 커스터마이징&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`.gemini/styleguide.me` 및 `config.yaml` 파일을 레포지토리의 루트 저장소에 추가하면 커스터마이징이 가능합니다.&lt;br /&gt;이와 관련해서는 구글에서 가이드를 제공해주고 있으니 아래 링크를 참고해 주세요.&lt;/p&gt;
&lt;figure id=&quot;og_1742786429580&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;GitHub에서 Gemini Code Assist 동작 맞춤설정 &amp;nbsp;|&amp;nbsp; Google for Developers&quot; data-og-description=&quot;Gemini Code Assist 동작을 맞춤설정하는 방법을 알아보세요.&quot; data-og-host=&quot;developers.google.com&quot; data-og-source-url=&quot;https://developers.google.com/gemini-code-assist/docs/customize-gemini-behavior-github?hl=ko#style-guide&quot; data-og-url=&quot;https://developers.google.com/gemini-code-assist/docs/customize-gemini-behavior-github?hl=ko&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dC3LKW/hyYvj7oNeA/HNUOjGNDDC2lafPzokkn7k/img.png?width=1200&amp;amp;height=675&amp;amp;face=0_0_1200_675&quot;&gt;&lt;a href=&quot;https://developers.google.com/gemini-code-assist/docs/customize-gemini-behavior-github?hl=ko#style-guide&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developers.google.com/gemini-code-assist/docs/customize-gemini-behavior-github?hl=ko#style-guide&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dC3LKW/hyYvj7oNeA/HNUOjGNDDC2lafPzokkn7k/img.png?width=1200&amp;amp;height=675&amp;amp;face=0_0_1200_675');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub에서 Gemini Code Assist 동작 맞춤설정 &amp;nbsp;|&amp;nbsp; Google for Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Gemini Code Assist 동작을 맞춤설정하는 방법을 알아보세요.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developers.google.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. 결론&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Github의 Gemini Code Assist 앱은 코드 리뷰의 품질과 효율을 동시에 높여줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히, 리뷰 리소스가 부족한 팀이나 스타트업 환경에서 &lt;b&gt;AI 기반의 자동 리뷰&lt;/b&gt;를 통해 빠른 피드백을 받을 수 있으며, `.gemini/styleguide.me` 파일을 활용하면 팀의 코딩 컨벤션에 맞춘 리뷰 커스터마이징도 가능합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;아쉬웠던 점&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재는 &lt;b&gt;미리보기(Preview) 버전&lt;/b&gt;이기 때문에 향후 기능 변경이나 서비스 정책의 불확실성이 존재합니다.&lt;/li&gt;
&lt;li&gt;한국어 지원이 제한적입니다. `styleguide.me` 파일에 한국어 사용을 유도하는 지침을 설정해 보았으나, 실제 리뷰 결과에서는 영어와 한국어가 혼용되는 경향이 있었습니다. 다행히 번역 도구를 활용하면 실사용에 큰 문제는 없었으며, 개선 여지를 기대해 볼 만합니다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Github/AI Code Review</category>
      <category>ai code review</category>
      <category>Github</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/68</guid>
      <comments>https://code-tales.tistory.com/68#entry68comment</comments>
      <pubDate>Tue, 25 Mar 2025 07:10:16 +0900</pubDate>
    </item>
    <item>
      <title>[Github] PR Template 전역으로 설정하여 유지보수 효율적으로 하기 #2 - 다중 템플릿</title>
      <link>https://code-tales.tistory.com/67</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 개요&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞선 글에서는 `.github` 레포지토리를 통해 Github 조직 내에서 &lt;b&gt;PR 템플릿을 전역&lt;/b&gt;으로 설정하는 방법에 대해 다루었습니다. 이를 통해 모든 프로젝트에서 일관된 PR 포맷을 자동으로 적용할 수 있었지만, 아쉬운 점이 한 가지 있었습니다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Github는 기본적으로 하나의 PR 템플릿만 지원&lt;/b&gt;하기 때문에 &lt;br /&gt;실무에서 자주 필요한 다양한 상황별 템플릿을 선택하여 사용하는 것이 불가능합니다.&lt;br /&gt;&lt;br /&gt;이번 글에서는 이러한 제약을 우회하여, &lt;b&gt;전역 PR 템플릿을 사용&lt;/b&gt;하면서도 &lt;b&gt;다중 템플릿을 선택적으로 사용하는 방법&lt;/b&gt;에 대해 소개하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;21.03.2025 15_15 (3).gif&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;628&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHGYML/btsMTl9TZpe/4HIkDoFbG4fbX4GuMzqy7k/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHGYML/btsMTl9TZpe/4HIkDoFbG4fbX4GuMzqy7k/img.gif&quot; data-alt=&quot;다중 템플릿을 선택하는 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHGYML/btsMTl9TZpe/4HIkDoFbG4fbX4GuMzqy7k/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bHGYML/btsMTl9TZpe/4HIkDoFbG4fbX4GuMzqy7k/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;393&quot; data-filename=&quot;21.03.2025 15_15 (3).gif&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;628&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;다중 템플릿을 선택하는 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 전역 환경에서 다중 템플릿을 구성하는 방법&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Github는 다중 템플릿 선택 UI를 제공하지 않기 때문에, 사용자가 직접 필요한 템플릿을 선택할 수 있도록 하는 방식으로 구현해야 합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2-1. pull_request_template.md 파일에 템플릿 링크 추가&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`.github/pull_request_template.md`파일 안에 여러 템플릿을 선택할 수 있는 링크를 넣습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1742536059800&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;`Preview` 탭을 누르고 템플릿을 선택해주세요.

* [기능 구현](?expand=1&amp;amp;template=feature_template.md)
* [버그 수정](?expand=1&amp;amp;template=bugfix_template.md)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 PR을 만들 때 위 마크다운이 보이게 되고, 사용자가 원하는 템플릿을 클릭하면 선택한 템플릿의 내용이 채워지게 됩니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2-2. 템플릿 파일을 별도 폴더에 생성&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;링크가 동작하려면 각각의 템플릿 파일을 `PULL_REQUEST_TEMPLATE`이라는 폴더 안에 개별 마크다운 파일로 저장해야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1742536278889&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.github/
└── .github/
    ├── pull_request_template.md &amp;larr; 메인 템플릿
    └── PULL_REQUEST_TEMPLATE/
        ├── feature_template.md   &amp;larr; 기능 구현용
        └── bugfix_template.md    &amp;larr; 버그 수정용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 간단하게 아래와 같이 기능 구현용, 버그 수정용 마크다운 파일을 추가해 주었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1742536356201&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// bugfix_template.md

# 작업내역
- [요약: ex. 기능 개선/버그 수정]
- [요약: ex. UI 개선/성능 최적화]

---

## 1. [수정 제목 또는 간단 설명]
-   문제점
  - 상세 문제 설명 기재
 
-   원인 분석
  - 발생 원인 설명
    
-   해결 방법
  - 적용한 해결 방안 설명

-   수정 전/후 비교
  |수정 전|수정 후|
  |:-:|:-:|
  |&amp;lt;video src=&quot;&quot; /&amp;gt;|&amp;lt;video src=&quot;&quot; /&amp;gt;|&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1742536404636&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// feature_template.md

# 작업 내역
- [요약: 신규 기능 추가]
- [요약: 기능 개선 등]

---

## 1. [기능 제목 또는 간단 설명]
- 상세 내용 기재
  
  |기능1|기능1|
  |:-:|:-:|
  |&amp;lt;img width=&quot;400&quot; src=&quot;스크린샷 URL&quot; /&amp;gt;|&amp;lt;img width=&quot;400&quot; src=&quot;스크린샷 URL&quot; /&amp;gt;|&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 최종 흐름 요약&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;PR을 생성할 때, `pull_request_template.md`는 항상 로드되며, 링크 목록을 제공함&lt;/li&gt;
&lt;li&gt;사용자는 `Preview`탭에서 원하는 템플릿을 선택함&lt;/li&gt;
&lt;li&gt;선택한 템플릿을 기반으로 PR을 작성함&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 구성하면 &lt;b&gt;Github의 전역 템플릿&lt;/b&gt;을 사용하면서도 &lt;b&gt;여러 템플릿 중 하나를 선택해서 사용할 수 있는 환경&lt;/b&gt;을 만들 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. 결론&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서 소개한 방식은 `pull_request_template.md`파일에 직접 링크를 제공하고, 사용자가 상황에 맞는 템플릿을 활용할 수 있도록 유도하는 방법입니다. Github 기본 기능만으로 구현 가능하며, 조직 전체에 일관성 있는 PR 템플릿을 적용할 수 있는 매우 실용적인 방법이라고 생각합니다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;1편에서 소개한 전역 PR 템플릿 설정 방법&lt;/b&gt;과 &lt;b&gt;본 글에서 다룬 다중 템플릿 선택 구조를 결합&lt;/b&gt;하여&lt;br /&gt;유지보수성과 일관성을 모두 갖춘 PR 작성 환경을 구축하는 데 도움이 되었기를 바랍니다.&lt;/p&gt;
&lt;figure id=&quot;og_1742537436584&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - coding-duck-org/.github&quot; data-og-description=&quot;Contribute to coding-duck-org/.github development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/coding-duck-org/.github/tree/2-ApplyMultipleTemplate&quot; data-og-url=&quot;https://github.com/coding-duck-org/.github&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/n5wkY/hyYrW6uP8l/Df256HhWqTMiFdJ4DQnXqk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/babpqA/hyYuhPq3Vw/tnovaoJjnPfyu7UNA9A5o0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/coding-duck-org/.github/tree/2-ApplyMultipleTemplate&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/coding-duck-org/.github/tree/2-ApplyMultipleTemplate&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/n5wkY/hyYrW6uP8l/Df256HhWqTMiFdJ4DQnXqk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/babpqA/hyYuhPq3Vw/tnovaoJjnPfyu7UNA9A5o0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - coding-duck-org/.github&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to coding-duck-org/.github development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Github/PR Template</category>
      <category>Github</category>
      <category>pr template</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/67</guid>
      <comments>https://code-tales.tistory.com/67#entry67comment</comments>
      <pubDate>Mon, 24 Mar 2025 07:10:53 +0900</pubDate>
    </item>
    <item>
      <title>[Github] PR Template 전역으로 설정하여 유지보수 효율적으로 하기</title>
      <link>https://code-tales.tistory.com/66</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 개요&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Github를 사용하는 조직은 일관된 PR을 작성하기 위하여 &lt;b&gt;PR Template&lt;/b&gt; 기능을 사용하면 좋습니다. &lt;br /&gt;&lt;br /&gt;하지만 전역으로 설정하는 방법을 모른다면 개별 레포지토리마다 PR 템플릿을 만들어야 하고,&lt;br /&gt;이는 유지보수를 어렵게 만듭니다.&lt;br /&gt;&lt;br /&gt;본 글에서는 Github 전역 PR 템플릿을 설정하는 방법에 대해 포스팅하겠습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;전역 PR 템플릿이 필요한 경우&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개별 레포지토리마다 PR 템플릿을 별도로 관리하고 있음&lt;/li&gt;
&lt;li&gt;모든 프로젝트에서 PR 형식의 일관성을 유지하고자 함&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;전역 PR 템플릿의 장점&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PR 형식을 조직 전체적으로 일관성 있게 유지 가능&lt;/li&gt;
&lt;li&gt;개별 레포지토리에 템플릿을 별도로 만들 필요가 없어서 관리가 쉬움&lt;/li&gt;
&lt;li&gt;리뷰어와의 소통 효율성을 증가&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. Github 전역 PR 템플릿 설정 방법&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 총 3단계를 통해 쉽게 설정할 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1단계: `.github` 특수 레포지토리 생성&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Github 조직(organization)에 `.github`라는 이름의 레포지토리를 생성합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;레포지토리는 반드시 `Public`으로 설정해야 합니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;생성 후, Github에서 자동으로 `special repository`라는 문구가 표시됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1535&quot; data-origin-height=&quot;681&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ueo9n/btsMQEimwyV/KZjyLKtYih6jFWYQKBRDNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ueo9n/btsMQEimwyV/KZjyLKtYih6jFWYQKBRDNk/img.png&quot; data-alt=&quot;.github 레포지토리 생성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ueo9n/btsMQEimwyV/KZjyLKtYih6jFWYQKBRDNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fueo9n%2FbtsMQEimwyV%2FKZjyLKtYih6jFWYQKBRDNk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;550&quot; height=&quot;244&quot; data-origin-width=&quot;1535&quot; data-origin-height=&quot;681&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;.github 레포지토리 생성&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2단계: `.github` 폴더 생성&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;생성한 `.github` 레포지토리 내부에 다시 `.github`라는 폴더를 생성합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;폴더 이름과 레포지토리 이름이 동일하므로 혼동하지 않도록 합니다!&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1535&quot; data-origin-height=&quot;681&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pkyA3/btsMRVwyEOv/BAkgoyCVjE8YRt5loQ7FZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pkyA3/btsMRVwyEOv/BAkgoyCVjE8YRt5loQ7FZ0/img.png&quot; data-alt=&quot;.github 폴더 추가&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pkyA3/btsMRVwyEOv/BAkgoyCVjE8YRt5loQ7FZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpkyA3%2FbtsMRVwyEOv%2FBAkgoyCVjE8YRt5loQ7FZ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;550&quot; height=&quot;244&quot; data-origin-width=&quot;1535&quot; data-origin-height=&quot;681&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;.github 폴더 추가&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3단계: `PULL_REQUEST_TEMPLATE.md` 파일 추가&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;`.github` 폴더 내에 `PULL_REQUEST_TEMPLATE.md` 파일을 추가하여,&lt;br /&gt;마크다운 언어로 PR 기본 템플릿을 작성합니다.&lt;/li&gt;
&lt;li&gt;저는 아래와 같이 기능 수정에 관련된 템플릿을 작성했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1742454945347&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 작업내역
- [요약: ex. 기능 개선/버그 수정]
- [요약: ex. UI 개선/성능 최적화]

---

## 1. [수정 제목 또는 간단 설명]
-   문제점
  - 상세 문제 설명 기재
 
-   원인 분석
  - 발생 원인 설명
    
-   해결 방법
  - 적용한 해결 방안 설명

-   수정 전/후 비교
  |수정 전|수정 후|
  |:-:|:-:|
  |&amp;lt;video src=&quot;&quot; /&amp;gt;|&amp;lt;video src=&quot;&quot; /&amp;gt;|


## 2. [수정 제목 또는 간단 설명]
-   문제점
  - 상세 문제 설명 기재
  
-   원인 분석
  - 발생 원인 설명

-   해결 방법
  - 적용한 해결 방안 설명

-   수정 전/후 비교
  |수정 전|수정 후|
  |:-:|:-:|
  |&amp;lt;video src=&quot;&quot; /&amp;gt;|&amp;lt;video src=&quot;&quot; /&amp;gt;|&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 적용된 모습 확인&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 설정을 완료하면 조직 내의 모든 레포지토리에서 PR을 작성할 때,&lt;br /&gt;자동으로 `.github &amp;gt; .github &amp;gt; PULL_REQUEST_TEMPLATE.md`에 작성한 템플릿을 자동으로 불러옵니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2176&quot; data-origin-height=&quot;1341&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7YLpR/btsMQFn18VB/1iKXLyyq7RXmOprKmlMv4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7YLpR/btsMQFn18VB/1iKXLyyq7RXmOprKmlMv4K/img.png&quot; data-alt=&quot;PR을 생성하면 자동으로 템플릿을 불러온다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7YLpR/btsMQFn18VB/1iKXLyyq7RXmOprKmlMv4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7YLpR%2FbtsMQFn18VB%2F1iKXLyyq7RXmOprKmlMv4K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;700&quot; height=&quot;431&quot; data-origin-width=&quot;2176&quot; data-origin-height=&quot;1341&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;PR을 생성하면 자동으로 템플릿을 불러온다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. 결론&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Github 전역 PR 템플릿 기능을 사용하면 관리 효율성이 크게 증가합니다.&lt;br /&gt;그러나 &lt;b&gt;Github는 기본적으로 단 하나의 템플릿&lt;/b&gt;만 설정할 수 있습니다.&lt;br /&gt;&lt;br /&gt;실무에서는 기능 구현, 버그 수정, 리팩토링 등 여러 상황에 따라 다양한 템플릿이 필요한 경우가 많습니다.&lt;br /&gt;다음 포스팅에서는 &lt;b&gt;여러 개의 PR 템플릿을 설정하여 사용하는 방법&lt;/b&gt;을 다루도록 하겠습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1742456743005&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - coding-duck-org/.github&quot; data-og-description=&quot;Contribute to coding-duck-org/.github development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/coding-duck-org/.github/tree/1-CreatePRTemplate&quot; data-og-url=&quot;https://github.com/coding-duck-org/.github&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/gqp1Q/hyYujzqtff/KkEbnKmokhtJ98TxEFrkRK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bSC50g/hyYvn2tcwE/l5ck3aoiBTsEhPjQEfapL0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/coding-duck-org/.github/tree/1-CreatePRTemplate&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/coding-duck-org/.github/tree/1-CreatePRTemplate&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/gqp1Q/hyYujzqtff/KkEbnKmokhtJ98TxEFrkRK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bSC50g/hyYvn2tcwE/l5ck3aoiBTsEhPjQEfapL0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - coding-duck-org/.github&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to coding-duck-org/.github development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Github/PR Template</category>
      <category>Github</category>
      <category>pr template</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/66</guid>
      <comments>https://code-tales.tistory.com/66#entry66comment</comments>
      <pubDate>Fri, 21 Mar 2025 10:00:49 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 멀티 모듈] 11. JvmLibraryConventionPlugin 만들기</title>
      <link>https://code-tales.tistory.com/65</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;지난 포스트에서는 Room 데이터베이스를 위한 AndoridRoomConventionPlugin을 만들었습니다.&lt;br /&gt;&lt;br /&gt;이번 시간에는 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;안드로이드에 의존성이 없는 모듈을 위한 JvmLibraryConventionPlugin&lt;/b&gt;&lt;/span&gt;을 만들겠습니다.&lt;br /&gt;해당 플러그인은 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;도메인 모듈 등&lt;/b&gt;&lt;/span&gt;에서 사용하게 될 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;670&quot; data-origin-height=&quot;510&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/95HI2/btsK1IVjMZr/kYkbkXyUSnbf7jW6eDY9s1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/95HI2/btsK1IVjMZr/kYkbkXyUSnbf7jW6eDY9s1/img.png&quot; data-alt=&quot;JvmLibraryConventionPlugin이 추가된 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/95HI2/btsK1IVjMZr/kYkbkXyUSnbf7jW6eDY9s1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F95HI2%2FbtsK1IVjMZr%2FkYkbkXyUSnbf7jW6eDY9s1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;450&quot; height=&quot;343&quot; data-origin-width=&quot;670&quot; data-origin-height=&quot;510&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;JvmLibraryConventionPlugin이 추가된 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. configureKotlinJvm 유틸 함수 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;build-logic 모듈의 Kotlin 파일에 Kotlin과 JVM 설정을 하기 위한 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Project.configureKotlinJvm 이라는 유틸 함수&lt;/b&gt;&lt;/span&gt;를 만들겠습니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;configureKotlinJvm 함수&lt;/b&gt;&lt;/span&gt;는 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Java 11&lt;/b&gt;&lt;/span&gt;을 사용하도록 설정하는 역할을 합니다. build-logic 모듈에서만 사용할 것 이므로 internal 키워드를 추가해주었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1732960274854&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Kotlin.kt

internal fun Project.configureKotlinJvm() {
    extensions.configure&amp;lt;JavaPluginExtension&amp;gt; {
        sourceCompatibility = JavaVersion.VERSION_11
        targetCompatibility = JavaVersion.VERSION_11
    }
    configureKotlin()
}

// 예전에 만들었던 함수
private fun Project.configureKotlin() {
    tasks.withType&amp;lt;KotlinCompile&amp;gt;().configureEach {
        kotlinOptions {
            jvmTarget = JavaVersion.VERSION_11.toString()
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. JvmLibraryConventionPlugin 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 후, JvmLibraryConventionPlugin을 아래처럼 만들었습니다. 위에서 만든 유틸함수를 호출하고 있는 모습입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1732960090880&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// JvmLibraryConventionPlugin.kt

class JvmLibraryConventionPlugin: Plugin&amp;lt;Project&amp;gt; {

    override fun apply(target: Project) {
        target.run {
            pluginManager.apply(&quot;org.jetbrains.kotlin.jvm&quot;)

            configureKotlinJvm()
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. JvmLibraryConventionPlugin 적용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gradle에게 특정 id로 컨벤션 플러그인을 인식할 수 있도록 해야합니다.&lt;br /&gt;libs.versions.toml 파일에 id를 정의하고 build-logic 모듈에 위에서 만든 컨벤션 플러그인을 등록합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1732960489893&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// libs.versions.toml

[plugins]
multi-module-jvm-library = { id = &quot;multi.module.jvm.library&quot;, version = &quot;unspecified&quot; }
...&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1732960514508&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// build.gradle.kts (Mobule :build-logic:convention)

...

gradlePlugin {
   plugins {  
      register(&quot;jvmLibrary&quot;) {
         id = &quot;multi.module.jvm.library&quot;
         implementationClass = &quot;JvmLibraryConventionPlugin&quot;
      }
      ...
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그 후, home:domain 모듈에 해당 컨벤션 플러그인을 적용하겠습니다. 제가 만든 domain 모듈은 안드로이드에 의존성이 없는 모듈이기 때문입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1732960731386&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// build.gradle.kts (Module :home:domain)

plugins {
    alias(libs.plugins.multi.module.jvm.library)
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;안드로이드에 의존성이 없고 순수 Java/Kotlin로 생성된 모듈&lt;/b&gt;&lt;/span&gt;을 위한 JvmLibraryConventionPlugin을 만들었습니다.&lt;br /&gt;&lt;br /&gt;최근 안드로이드에서는 클린 아키텍처로 구성된 프로젝트가 많습니다. 그리고 domain 레이어를 분리해서 사용하는 경우가 많은데, 이러한 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;domain 레이어는 안드로이드에 의존성이 없도록 만드는 것이 원칙&lt;/b&gt;&lt;/span&gt;입니다.&lt;br /&gt;&lt;br /&gt;그래서 JvmLibraryConventionPlugin을 만들게 되었습니다.&lt;br /&gt;&lt;br /&gt;이번 포스팅의 결과물은 아래 Github Repository의 11-JvmLibraryConventionPlugin 브랜치를 확인하시면 됩니다.&lt;/p&gt;
&lt;figure id=&quot;og_1732961010784&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - taein8935/multi-module-template-aos&quot; data-og-description=&quot;Contribute to taein8935/multi-module-template-aos development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/taein8935/multi-module-template-aos/tree/11-JvmLibraryConventionPlugin&quot; data-og-url=&quot;https://github.com/taein8935/multi-module-template-aos&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dV8iIS/hyXGzitgmA/kYM04vYcuWG8cqLE3Dzupk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/sk7LN/hyXGDypSiu/Nsfxzm7L8dWuw0MxPPuV91/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/taein8935/multi-module-template-aos/tree/11-JvmLibraryConventionPlugin&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/taein8935/multi-module-template-aos/tree/11-JvmLibraryConventionPlugin&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dV8iIS/hyXGzitgmA/kYM04vYcuWG8cqLE3Dzupk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/sk7LN/hyXGDypSiu/Nsfxzm7L8dWuw0MxPPuV91/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - taein8935/multi-module-template-aos&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to taein8935/multi-module-template-aos development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>안드로이드/멀티 모듈</category>
      <category>Gradle</category>
      <category>multi module</category>
      <category>멀티 모듈</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/65</guid>
      <comments>https://code-tales.tistory.com/65#entry65comment</comments>
      <pubDate>Thu, 19 Dec 2024 11:00:11 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 멀티 모듈] 10. AndroidRoomConventionPlugin 만들기</title>
      <link>https://code-tales.tistory.com/64</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;지난 포스트에서는 Compose로 구성된 화면을 위한 AndroidPresentationUIConventionPlugin을 만들었습니다.&lt;br /&gt;&lt;br /&gt;이번 시간에는 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Room 데이터베이스&lt;/b&gt;&lt;/span&gt;를 사용하기 위한 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;AndroidRoomConventionPlugin&lt;/b&gt;&lt;/span&gt;을 만들겠습니다. &lt;br /&gt;해당 플러그인은 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;데이터베이스와 관련된 모듈&lt;/b&gt;&lt;/span&gt;에서 적용하는 컨벤션 플러그인 입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;666&quot; data-origin-height=&quot;467&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pVVu1/btsK2k7e3hM/9LOK5RVQoKLppHx1K3AIuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pVVu1/btsK2k7e3hM/9LOK5RVQoKLppHx1K3AIuk/img.png&quot; data-alt=&quot;AndroidRoomConventionPlugin이 추가된 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pVVu1/btsK2k7e3hM/9LOK5RVQoKLppHx1K3AIuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpVVu1%2FbtsK2k7e3hM%2F9LOK5RVQoKLppHx1K3AIuk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;450&quot; height=&quot;316&quot; data-origin-width=&quot;666&quot; data-origin-height=&quot;467&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;AndroidRoomConventionPlugin이 추가된 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Room과 관련된 의존성 추가하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, libs.versions.toml 파일에 아래와 같이 Room을 사용하기 위한 라이브러리와 플러그인을 추가하겠습니다.&lt;br /&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;room-compiler, room-ktx, room-runtime, room-gradle-plugin&lt;/b&gt;&lt;/span&gt;로 구성된 4개의 라이브러리와 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;androidx.room 플러그인&lt;/b&gt;&lt;/span&gt;을 추가합니다.&lt;br /&gt;&lt;br /&gt;Room 데이터베이스는 &lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;ksp&lt;/span&gt;&lt;/b&gt;도 필요하므로 함께 추가해주겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1732956449092&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// libs.versions.toml

[versions]
room = &quot;2.6.1&quot;
ksp = &quot;1.9.22-1.0.17&quot; // 코틀린 버전에 맞춰야함
...


[libraries]
room-compiler = { group = &quot;androidx.room&quot;, name = &quot;room-compiler&quot;, version.ref = &quot;room&quot; }
room-ktx = { group = &quot;androidx.room&quot;, name = &quot;room-ktx&quot;, version.ref = &quot;room&quot; }
room-runtime = { group = &quot;androidx.room&quot;, name = &quot;room-runtime&quot;, version.ref = &quot;room&quot; }
room-gradlePlugin = { group = &quot;androidx.room&quot;, name = &quot;room-gradle-plugin&quot;, version.ref = &quot;room&quot; }
ksp-gradlePlugin = { group = &quot;com.google.devtools.ksp&quot;, name = &quot;com.google.devtools.ksp.gradle.plugin&quot;, version.ref = &quot;ksp&quot; }
...

[plugins]
room = { id = &quot;androidx.room&quot;, version.ref = &quot;room&quot; }
ksp = { id = &quot;com.google.devtools.ksp&quot;, version.ref = &quot;ksp&quot; }
...&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. AndroidRoomConventionPlugin 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 AndroidRoomConventionPlugin을 만들겠습니다. 위 과정에서 추가한 의존성들을 해당 컨벤션 플러그인에 적용해야 합니다.&lt;br /&gt;&lt;br /&gt;pluginManager를 통해서 androidx.room 및 ksp 플러그인을 적용하고 dependencies 블록에 Room과 관련된 의존성을 추가한 모습입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1732956733570&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// AndroidRoomConventionPlugin.kt

class AndroidRoomConventionPlugin: Plugin&amp;lt;Project&amp;gt; {

    override fun apply(target: Project) {
        target.run {
            pluginManager.run {
                apply(&quot;androidx.room&quot;)
                apply(&quot;com.google.devtools.ksp&quot;)
            }

            extensions.configure&amp;lt;RoomExtension&amp;gt; {
                schemaDirectory(&quot;$projectDir/schemas&quot;)
            }

            dependencies {
                &quot;implementation&quot;(libs.findLibrary(&quot;room.runtime&quot;).get())
                &quot;implementation&quot;(libs.findLibrary(&quot;room.ktx&quot;).get())
                &quot;ksp&quot;(libs.findLibrary(&quot;room.compiler&quot;).get())
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;다른 컨벤션 플러그인과 다르게 Room 플러그인의 확장 설정을 구성한 모습을 볼 수 있습니다. &lt;br /&gt;&lt;br /&gt;바로 extensions.configure&amp;lt;RoomExtension&amp;gt; 블록인데, &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Room 데이터베이스 스키마를 저장할 디렉토리를 설정&lt;/b&gt;&lt;/span&gt;해주는 코드입니다. 여기서는 해당 컨벤션 플러그인을 사용하는 모듈의 루트 경로에 schemas 폴더로 지정해주었습니다.&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;3. AndroidRoomConventionPlugin 적용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 프로젝트 레벨의 build.gradle.kts 파일에 ksp와 room 플러그인을 사용할 것 이라고 명시해주겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1732958223020&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// build.gradle.kts (Project: MultiModuleTemplate)
plugins {
    alias(libs.plugins.ksp) apply false
    alias(libs.plugins.room) apply false
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gradle에서 컨벤션 플러그인을 인식하기 위해서 libs.versions.toml에 id를 추가하겠습니다.&lt;br /&gt;그리고 build-logic에도 id와 컨벤션 플러그인 이름을 사용하여 추가하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1732957323111&quot; class=&quot;openscad&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// libs.versions.toml

[plugins]
multi-module-android-room = { id = &quot;multi.module.android.room&quot;, version = &quot;unspecified&quot; }
...&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1732957429599&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// build.gradle.kts (Module :build-logic-convention)

gradlePlugin {
   plugins {
      ...
      register(&quot;androidRoom&quot;) {
         id = &quot;multi.module.android.room&quot;
         implementationClass = &quot;AndroidRoomConventionPlugin&quot;
      }
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그리고 Room 데이터베이스를 사용하려는 모듈에 AndroidRoomConventionPlugin을 적용하면 됩니다. 여기서는 database 모듈에 적용해주었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1732958332137&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// build.gradle.kts (Module: core:database)

plugins {
    alias(libs.plugins.multi.module.android.library)
    alias(libs.plugins.multi.module.android.room)
}

android {
    namespace = &quot;com.multi.module.core.database&quot;
}

dependencies {
  ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Room 데이터베이스를 사용하기 위한 AndroidRoomConventionPlugin을 만들었습니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Room은 ksp를 사용하기 때문에 ksp와 관련된 설정&lt;/b&gt;&lt;/span&gt;도 잊지 않고 같이 해주어야 합니다.&lt;br /&gt;&lt;br /&gt;다음 포스팅에서는 안드로이드에 의존성이 없는 도메인 모듈을 위한 JvmLibraryConventionPlugin을 만들겠습니다.&lt;br /&gt;&lt;br /&gt;이번 포스팅의 결과물은 아래 Github Repository의 10-AndroidRoomConventionPlugin 브랜치를 확인하시면 됩니다.&lt;/p&gt;
&lt;figure id=&quot;og_1732958533585&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - taein8935/multi-module-template-aos&quot; data-og-description=&quot;Contribute to taein8935/multi-module-template-aos development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/taein8935/multi-module-template-aos/tree/10-AndroidRoomConventionPlugin&quot; data-og-url=&quot;https://github.com/taein8935/multi-module-template-aos&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/BCC4i/hyXGAuV3xP/LDC67yRJ1uZXxMlI3nBc2K/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/boPPXM/hyXGOUdBB4/3czXXHp0UcIaK55fS6aYm1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/taein8935/multi-module-template-aos/tree/10-AndroidRoomConventionPlugin&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/taein8935/multi-module-template-aos/tree/10-AndroidRoomConventionPlugin&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/BCC4i/hyXGAuV3xP/LDC67yRJ1uZXxMlI3nBc2K/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/boPPXM/hyXGOUdBB4/3czXXHp0UcIaK55fS6aYm1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - taein8935/multi-module-template-aos&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to taein8935/multi-module-template-aos development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>안드로이드/멀티 모듈</category>
      <category>Gradle</category>
      <category>multi module</category>
      <category>멀티 모듈</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/64</guid>
      <comments>https://code-tales.tistory.com/64#entry64comment</comments>
      <pubDate>Mon, 16 Dec 2024 11:00:08 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 멀티 모듈] 9. AndroidPresentationUIConventionPlugin 만들기</title>
      <link>https://code-tales.tistory.com/63</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;지난 포스팅에서는 화면은 없지만 Compose와 관련된 컨벤션 플러그인을 만들어서 designsystem 모듈에 적용했습니다.&lt;br&gt;&lt;br&gt;이번 포스팅에서는 &lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;화면이 존재하고 Compose와 관련된 모듈을 위한 AndroidPresentationUIConventionPlugin&lt;/span&gt;&lt;/b&gt;을 만들겠습니다.&lt;br&gt;&lt;br&gt;해당 플러그인은 home, login, board 등 &lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;화면을 담당하는 UI Layer 모듈에 적용되는 컨벤션&lt;/span&gt; 플러그인&lt;/b&gt;입니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;423&quot; data-origin-height=&quot;385&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XMGOu/btsI5OW0eyC/HOH0J4PsNh8QKvIUoTgzi0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XMGOu/btsI5OW0eyC/HOH0J4PsNh8QKvIUoTgzi0/img.png&quot; data-alt=&quot;AndroidPresentationUIConventionPlugin 및 ComposeDependencies가 추가된 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XMGOu/btsI5OW0eyC/HOH0J4PsNh8QKvIUoTgzi0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXMGOu%2FbtsI5OW0eyC%2FHOH0J4PsNh8QKvIUoTgzi0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;AndroidPresentationUIConventionPlugin 및 ComposeDependencies가 추가된 모습&quot; loading=&quot;lazy&quot; width=&quot;580&quot; height=&quot;528&quot; data-origin-width=&quot;423&quot; data-origin-height=&quot;385&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;AndroidPresentationUIConventionPlugin 및 ComposeDependencies가 추가된 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. AndroidPresentationUIConventionPlugin 만들기&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 화면이 존재하는 Compose 컨벤션 플러그인을 만들고 있습니다.&lt;br&gt;그래서 이전 포스팅에서 만든 컨벤션 플러그인을 pluginManager를 통해 적용하고, Compose 화면을 위한 의존성을 추가해줘야 합니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// AndroidPresentationUIConventionPlugin.kt
class AndroidPresentationUIConventionPlugin: Plugin&amp;lt;Project&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;override fun apply(target: Project) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;target.run {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;pluginManager.run {
&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;apply(&quot;multi.module.android.library.compose&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;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;dependencies {
&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;addUILayerDependencies(target)
&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;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;재사용성을 위해서 Compose 화면과 관련된 의존성을 &lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;addUILayerDependencies&lt;/span&gt;&lt;/b&gt;라는 유틸 함수를 통해 추가했습니다. &lt;br&gt;유틸함수는 아래와 같이 &lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;dependencies 블록 스코프에 대한 확장함수&lt;/span&gt;&lt;/b&gt;로 구현했습니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// ComposeDependencies.kt
fun DependencyHandlerScope.addUILayerDependencies(project: Project) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;add(&quot;implementation&quot;, project(&quot;:core:presentation:designsystem&quot;))

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;add(&quot;implementation&quot;, project.libs.findBundle(&quot;compose&quot;).get())
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;add(&quot;debugImplementation&quot;, project.libs.findBundle(&quot;compose.debug&quot;).get())
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;add(&quot;androidTestImplementation&quot;, project.libs.findLibrary(&quot;androidx.ui.test.junit4&quot;).get())
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;&lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;화면과 관련된 모듈은 designsystem 모듈에 의존&lt;/span&gt;&lt;/b&gt;하고 있으며, Compose 관련 의존성은 관리를 편하게 하기 위해서 아래와 같이 &lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;libs.versions.toml 파일에 bundles&lt;/span&gt;&lt;/b&gt;로 묶어서 처리했습니다. 자세한 코드는 포스팅 하단의 github repository를 확인해 보시면 됩니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// libs.versions.toml
...

[libraries]
...
androidx-activity-compose = { group = &quot;androidx.activity&quot;, name = &quot;activity-compose&quot;, version.ref = &quot;activityCompose&quot; }
androidx-lifecycle-runtime-compose = { module = &quot;androidx.lifecycle:lifecycle-runtime-compose&quot;, version.ref = &quot;lifecycleRuntimeKtx&quot; }
androidx-lifecycle-viewmodel-compose = { module = &quot;androidx.lifecycle:lifecycle-viewmodel-compose&quot;, version.ref = &quot;lifecycleRuntimeKtx&quot; }
androidx-ui = { group = &quot;androidx.compose.ui&quot;, name = &quot;ui&quot; }
androidx-ui-graphics = { group = &quot;androidx.compose.ui&quot;, name = &quot;ui-graphics&quot; }
androidx-ui-tooling = { group = &quot;androidx.compose.ui&quot;, name = &quot;ui-tooling&quot; }
androidx-ui-tooling-preview = { group = &quot;androidx.compose.ui&quot;, name = &quot;ui-tooling-preview&quot; }
androidx-ui-test-manifest = { group = &quot;androidx.compose.ui&quot;, name = &quot;ui-test-manifest&quot; }
androidx-ui-test-junit4 = { group = &quot;androidx.compose.ui&quot;, name = &quot;ui-test-junit4&quot; }
androidx-material3 = { group = &quot;androidx.compose.material3&quot;, name = &quot;material3&quot; }
...

[bundles]
compose = [
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;androidx-activity-compose&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;androidx-lifecycle-runtime-compose&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;androidx-lifecycle-viewmodel-compose&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;androidx-material3&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;androidx-ui&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;androidx-ui-graphics&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;androidx-ui-tooling-preview&quot;,
]
compose-debug = [
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;androidx-ui-tooling&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;androidx-ui-test-manifest&quot;,
]&lt;/code&gt;&lt;/pre&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. AndroidPresentationUIConventionPlugin 적용하기&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Gradle에서 컨벤션 플러그인을 인식하기 위해서 libs.versions.toml에 id를 추가하고, build-logic 모듈에 추가하겠습니다. 해당 단계에서 하는 작업은 이전 포스팅과 동일합니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// libs.versions.toml
...

# Custom Convention Plugin
...
multi-module-android-presentation-ui = { id = &quot;multi.module.android.presentation.ui&quot;, version = &quot;unspecified&quot; }&lt;/code&gt;&lt;/pre&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// build.gradle.kts (Module :build-logic-convention)
...

gradlePlugin {
&amp;nbsp;&amp;nbsp; plugins {
&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;register(&quot;androidPresentationUI&quot;) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; id = &quot;multi.module.android.presentation.ui&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; implementationClass = &quot;AndroidPresentationUIConventionPlugin&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp; }
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;그리고 Compose 화면을 위한 모듈에 AndroidPresentationUIConventionPlugin을 적용합니다.&lt;br&gt;여기서는 home:presentation 모듈뿐이지만 login, mypage, board 등 다양한 모듈에 적용할 수 있습니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// build.gradle.kts (:home:presentation)
plugins {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;alias(libs.plugins.multi.module.android.presentation.ui)
}

android {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;namespace = &quot;com.multi.module.home.presentation&quot;
}

dependencies {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;implementation(projects.home.domain)
}&lt;/code&gt;&lt;/pre&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 정리&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Compose로 구성된 Presentation 레이어를 위한 AndroidPresentationUIConventionPlugin을 만들었습니다.&lt;br&gt;&lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;실제 프로젝트에서 멀티 모듈 아키텍처를 구현했다면 해당 컨벤션 플러그인이 가장 많이 사용&lt;/span&gt;&lt;/b&gt;될 것 같습니다.&lt;br&gt;&lt;br&gt;다음 포스팅에서는 Room, Hilt 등을 위한 컨벤션 플러그인을 만들 예정입니다.&lt;br&gt;&lt;br&gt;이번 포스팅의 결과물은 아래 Github Repository의 9-AndroidPresentationUIConventionPlugin 브랜치를 확인하시면 됩니다.&lt;/p&gt;&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;GitHub - taein8935/multi-module-template-aos&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;Contribute to taein8935/multi-module-template-aos development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/taein8935/multi-module-template-aos/tree/9-AndroidPresentationUIConventionPlugin&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/kJo1D/hyWSpHimFN/vK6uvhK3K1fzRwiX0KaKR1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot; data-og-url=&quot;https://github.com/taein8935/multi-module-template-aos&quot;&gt;&lt;a href=&quot;https://github.com/taein8935/multi-module-template-aos&quot; target=&quot;_blank&quot; data-source-url=&quot;https://github.com/taein8935/multi-module-template-aos/tree/9-AndroidPresentationUIConventionPlugin&quot;&gt;&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/kJo1D/hyWSpHimFN/vK6uvhK3K1fzRwiX0KaKR1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600')&quot;&gt; &lt;/div&gt;&lt;div class=&quot;og-text&quot;&gt;&lt;p class=&quot;og-title&quot;&gt;GitHub - taein8935/multi-module-template-aos&lt;/p&gt;&lt;p class=&quot;og-desc&quot;&gt;Contribute to taein8935/multi-module-template-aos development by creating an account on GitHub.&lt;/p&gt;&lt;p class=&quot;og-host&quot;&gt;github.com&lt;/p&gt;&lt;/div&gt;&lt;/a&gt;&lt;/figure&gt;</description>
      <category>안드로이드/멀티 모듈</category>
      <category>Gradle</category>
      <category>multi module</category>
      <category>멀티 모듈</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/63</guid>
      <comments>https://code-tales.tistory.com/63#entry63comment</comments>
      <pubDate>Mon, 26 Aug 2024 11:00:14 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 멀티 모듈] 8. AndroidLibraryComposeConventionPlugin 만들기</title>
      <link>https://code-tales.tistory.com/62</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;지난 포스팅에서는 Compose가 없는 AndroidLibrary를 위한 AndroidLibraryConventionPlugin을 만들었습니다.&lt;br&gt;이번에는 &lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;Compose 관련 빌드 옵션이 포함&lt;/span&gt;&lt;/b&gt;된 AndroidLibraryComposeConventionPlugin을 만들겠습니다.&lt;br&gt;&lt;br&gt;&lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;Compose 관련 모듈은 아래처럼 2가지&lt;/span&gt;&lt;/b&gt;로 나뉘게 됩니다.&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;화면이 없는 모듈 (designsystem, ui 등)&lt;/li&gt;&lt;li&gt;화면이 있는 모듈 (home, board, login 등)&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;designsystem처럼 화면이 없는 모듈&lt;/span&gt;&lt;/b&gt;이 있고, &lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;home처럼 Compose로 구성된 화면이 있는 presentation 성격의 모듈&lt;/span&gt;&lt;/b&gt;이 있습니다.&lt;br&gt;&lt;br&gt;이번 포스팅에서는 &lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;Compose 관련 기능이 있지만 화면이 없는 모듈&lt;/span&gt;&lt;/b&gt;을 위한 컨벤션 플러그인을 만들겠습니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;456&quot; data-origin-height=&quot;345&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chZPN8/btsI7f0fjGq/8Ebnh0Van1v2R4vpToI3A0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chZPN8/btsI7f0fjGq/8Ebnh0Van1v2R4vpToI3A0/img.png&quot; data-alt=&quot;AndroidLibraryComposeConventionPlugin이 추가된 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chZPN8/btsI7f0fjGq/8Ebnh0Van1v2R4vpToI3A0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchZPN8%2FbtsI7f0fjGq%2F8Ebnh0Van1v2R4vpToI3A0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;AndroidLibraryComposeConventionPlugin이 추가된 모습&quot; loading=&quot;lazy&quot; width=&quot;580&quot; height=&quot;439&quot; data-origin-width=&quot;456&quot; data-origin-height=&quot;345&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;AndroidLibraryComposeConventionPlugin이 추가된 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. AndroidLibraryComposeConventionPlugin 만들기&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;지난 포스팅에서 만들어놓은 컨벤션 플러그인과 유틸 함수를 사용하면 아래처럼 간단하게 만들 수 있습니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// AndroidLibraryComposeConventionPlugin.kt
class AndroidLibraryComposeConventionPlugin: Plugin&amp;lt;Project&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;override fun apply(target: Project) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;target.run {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;pluginManager.run {
&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;apply(&quot;multi.module.android.library&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;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;val extension = extensions.getByType&amp;lt;LibraryExtension&amp;gt;()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;configureAndroidCompose(extension)
&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;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;pluginManager를 통해서 플러그인을 적용시켰는데, 이름을 보면 아시겠지만 &lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;지난 포스팅에서 만든 컨벤션 플러그인&lt;/span&gt;&lt;/b&gt;의 이름입니다. 즉, AndroidLibrary를 위한 컨벤션 플러그인을 적용시켰습니다.&lt;br&gt;&lt;br&gt;그리고 이전에 만든 &lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;configureAndroidCompose 유틸함수&lt;/span&gt;&lt;/b&gt;를 불러와서 Compose와 관련된 빌드 옵션을 적용시켰습니다.&lt;br&gt;&lt;br&gt;&lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;공통으로 사용할 수 있는 부분을 유틸 함수 및 컨벤션 플러그인으로 분리한 덕분에 AndroidLibraryComposeConventionPlugin을 간단하게 구성&lt;/span&gt;&lt;/b&gt;할 수 있습니다.&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. AndroidLibraryComposeConventionPlugin 적용하기&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이전과 마찬가지로 Gradle에서 컨벤션 플러그인을 인식할 수 있도록 libs.versions.toml 파일에 id를 추가하고, build-logic 모듈에 컨벤션 플러그인을 등록하겠습니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// libs.versions.toml
...

# Custom Convention Plugin
multi-module-android-application = { id = &quot;multi.module.android.application&quot;, version = &quot;unspecified&quot; }
multi-module-android-application-compose = { id = &quot;multi.module.android.application.compose&quot;, version = &quot;unspecified&quot; }
multi-module-android-library = { id = &quot;multi.module.android.library&quot;, version = &quot;unspecified&quot; }
multi-module-android-library-compose = { id = &quot;multi.module.android.library.compose&quot;, version = &quot;unspecified&quot; }&lt;/code&gt;&lt;/pre&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// build.gradle.kts (Module :build-logic:convention)
...

gradlePlugin {
&amp;nbsp;&amp;nbsp; plugins {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;register(&quot;androidApplication&quot;) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; id = &quot;multi.module.android.application&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; implementationClass = &quot;AndroidApplicationConventionPlugin&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;register(&quot;androidApplicationCompose&quot;) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; id = &quot;multi.module.android.application.compose&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; implementationClass = &quot;AndroidApplicationComposeConventionPlugin&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;register(&quot;androidLibrary&quot;) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; id = &quot;multi.module.android.library&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; implementationClass = &quot;AndroidLibraryConventionPlugin&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;register(&quot;androidLibraryCompose&quot;) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; id = &quot;multi.module.android.library.compose&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; implementationClass = &quot;AndroidLibraryComposeConventionPlugin&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp; }
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;이번 포스팅에서 만든 컨벤션 플러그인은 위에서 언급했듯이, 화면이 없지만 Compose와 관련된 AndroidLibrary를 위한 것입니다. 그래서 저는 designsystem 모듈에 해당 컨벤션 플러그인을 적용하겠습니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// build.gradle.kts (:core:presentation:designsystem)
plugins {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;alias(libs.plugins.multi.module.android.library.compose)
}

android {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;namespace = &quot;com.multi.module.core.presentation.designsystem&quot;
}

dependencies {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...
}&lt;/code&gt;&lt;/pre&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 정리&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;화면은 없지만 Compose를 사용하는 모듈&lt;/span&gt;&lt;/b&gt;에 적용할 수 있는 &lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;AndroidLibraryComposeConventionPlugin&lt;/span&gt;&lt;/b&gt;을 만들었습니다.&amp;nbsp;&lt;br&gt;&lt;br&gt;다음 포스팅에서는 Compose로 화면을 구성하는 모듈을 위한 컨벤션 플러그인을 만들겠습니다.&lt;br&gt;&lt;br&gt;이번 포스팅의 결과물은 아래 Github Repository의 8-AndroidLibraryComposeConventionPlugin 브랜치를 확인하시면 됩니다.&lt;/p&gt;&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;GitHub - taein8935/multi-module-template-aos&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;Contribute to taein8935/multi-module-template-aos development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/taein8935/multi-module-template-aos/tree/8-AndroidLibraryComposeConventionPlugin&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cyW8oC/hyWOgkU4q8/gGVX8JfKKN7dta11ikp6e0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot; data-og-url=&quot;https://github.com/taein8935/multi-module-template-aos&quot;&gt;&lt;a href=&quot;https://github.com/taein8935/multi-module-template-aos&quot; target=&quot;_blank&quot; data-source-url=&quot;https://github.com/taein8935/multi-module-template-aos/tree/8-AndroidLibraryComposeConventionPlugin&quot;&gt;&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cyW8oC/hyWOgkU4q8/gGVX8JfKKN7dta11ikp6e0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600')&quot;&gt; &lt;/div&gt;&lt;div class=&quot;og-text&quot;&gt;&lt;p class=&quot;og-title&quot;&gt;GitHub - taein8935/multi-module-template-aos&lt;/p&gt;&lt;p class=&quot;og-desc&quot;&gt;Contribute to taein8935/multi-module-template-aos development by creating an account on GitHub.&lt;/p&gt;&lt;p class=&quot;og-host&quot;&gt;github.com&lt;/p&gt;&lt;/div&gt;&lt;/a&gt;&lt;/figure&gt;</description>
      <category>안드로이드/멀티 모듈</category>
      <category>Gradle</category>
      <category>multi module</category>
      <category>멀티 모듈</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/62</guid>
      <comments>https://code-tales.tistory.com/62#entry62comment</comments>
      <pubDate>Mon, 19 Aug 2024 11:00:54 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 멀티 모듈] 7. AndroidLibraryConventionPlugin 만들기</title>
      <link>https://code-tales.tistory.com/61</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;지난 포스팅까지는 Application 모듈에 대한 컨벤션 플러그인을 다루었는데, 이번 포스팅에서는 &lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;Compose가 없는 AndroidLibrary를 위한 AndroidLibraryConventionPlugin&lt;/span&gt;&lt;/b&gt;을 만들겠습니다. 안드로이드 프로젝트를 멀티모듈로 구성한다면 꼭 필요한 컨벤션 플러그인입니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;407&quot; data-origin-height=&quot;328&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dqxsq6/btsIWxtA0Pa/ahBBdbhrlsSnZDwZ56fXP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dqxsq6/btsIWxtA0Pa/ahBBdbhrlsSnZDwZ56fXP1/img.png&quot; data-alt=&quot;AndroidLibraryConventionPlugin이 추가된 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dqxsq6/btsIWxtA0Pa/ahBBdbhrlsSnZDwZ56fXP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdqxsq6%2FbtsIWxtA0Pa%2FahBBdbhrlsSnZDwZ56fXP1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;AndroidLibraryConventionPlugin이 추가된 모습&quot; loading=&quot;lazy&quot; width=&quot;580&quot; height=&quot;467&quot; data-origin-width=&quot;407&quot; data-origin-height=&quot;328&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;AndroidLibraryConventionPlugin이 추가된 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. AndroidLibraryConventionPlugin 만들기&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이전 포스팅에서 확장성을 고려하여 여러 가지 유틸함수를 만든 덕분에 AndroidLibraryConventionPlugin을 간단하게 만들 수 있습니다. Build Type 설정을 위해 유틸함수를 만들었었는데, Enum Class로 ExtensionType에 대한 분기처리를 했기 때문에 아래처럼 ExtensionType.LIBRARY를 전달하면 AndroidLibrary용 Build Variant를 구성할 수 있게 됩니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// AndroidLibraryConventionPlugin.kt
class AndroidLibraryConventionPlugin: Plugin&amp;lt;Project&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;override fun apply(target: Project) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;target.run {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;pluginManager.run {
&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;apply(&quot;com.android.library&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;apply(&quot;org.jetbrains.kotlin.android&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;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;extensions.configure&amp;lt;LibraryExtension&amp;gt; {
&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;configureKotlinAndroid(this)

&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;configureBuildTypes(
&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;commonExtension = this,
&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;extensionType = ExtensionType.LIBRARY
&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;&amp;nbsp;&amp;nbsp;defaultConfig {
&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;testInstrumentationRunner = &quot;androidx.test.runner.AndroidJUnitRunner&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;consumerProguardFiles(&quot;consumer-rules.pro&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;&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;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;유틸 함수 및 전체 프로젝트 구조는 &lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;포스팅 하단의 Github Repository에서 소스코드를 확인&lt;/span&gt;&lt;/b&gt;하시면 됩니다.&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. AndroidLibraryConventionPlugin 적용하기&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Gradle이 컨벤션 플러그인을 인식할 수 있도록 libs.versions.toml 파일에 id를 정의하고, build-logic 모듈에서 AndroidLibraryConventionPlugin을 등록합니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// libs.versions.toml
...

# Custom Convention Plugin
multi-module-android-application = { id = &quot;multi.module.android.application&quot;, version = &quot;unspecified&quot; }
multi-module-android-application-compose = { id = &quot;multi.module.android.application.compose&quot;, version = &quot;unspecified&quot; }
multi-module-android-library = { id = &quot;multi.module.android.library&quot;, version = &quot;unspecified&quot; }&lt;/code&gt;&lt;/pre&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// build.gradle.kts (Module :build-logic:convention)
...

gradlePlugin {
&amp;nbsp;&amp;nbsp; plugins {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;register(&quot;androidApplication&quot;) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; id = &quot;multi.module.android.application&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; implementationClass = &quot;AndroidApplicationConventionPlugin&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;register(&quot;androidApplicationCompose&quot;) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; id = &quot;multi.module.android.application.compose&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; implementationClass = &quot;AndroidApplicationComposeConventionPlugin&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;register(&quot;androidLibrary&quot;) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; id = &quot;multi.module.android.library&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; implementationClass = &quot;AndroidLibraryConventionPlugin&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp; }
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;&lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;현재 만든 컨벤션 플러그인은 Compose 관련 코드가 없는 AndroidLibrary 컨벤션 플러그인&lt;/span&gt;&lt;/b&gt;입니다. 즉, XML 기반의 모듈이나 화면이 없는 data 모듈에서 사용하는 플러그인입니다. 여기서는 Compose 프로젝트로 생성했으므로 data 모듈에만 해당 컨벤션 플러그인을 적용하겠습니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// build.gradle.kts (:home:data)
plugins {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;alias(libs.plugins.multi.module.android.library)
}

android {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;namespace = &quot;com.multi.module.home.data&quot;
}

dependencies {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;implementation(projects.home.domain)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;컨벤션 플러그인을 적용하기 전의 모습과 비교한다면 &lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;많은 중복코드가 제거&lt;/span&gt;&lt;/b&gt;된 것을 확인할 수 있습니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1352&quot; data-origin-height=&quot;855&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VLNi1/btsIULNmwNF/zmFsGOsxIfl4GZhEf6rno0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VLNi1/btsIULNmwNF/zmFsGOsxIfl4GZhEf6rno0/img.png&quot; data-alt=&quot;컨벤션 플러그인 적용 전/후 코드 차이 비교&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VLNi1/btsIULNmwNF/zmFsGOsxIfl4GZhEf6rno0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVLNi1%2FbtsIULNmwNF%2FzmFsGOsxIfl4GZhEf6rno0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;컨벤션 플러그인 적용 전/후 코드 차이 비교&quot; loading=&quot;lazy&quot; width=&quot;1352&quot; height=&quot;855&quot; data-origin-width=&quot;1352&quot; data-origin-height=&quot;855&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;컨벤션 플러그인 적용 전/후 코드 차이 비교&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 정리&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;XML 기반의 모듈&lt;/span&gt;&lt;/b&gt;이나&lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt; data 모듈&lt;/span&gt;&lt;/b&gt;처럼 &lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;Compose를 사용하지 않는 모듈&lt;/span&gt;&lt;/b&gt;에 적용할 수 있는 AndroidLibraryConventionPlugin을 만들었습니다.&lt;br&gt;&lt;br&gt;다음 포스팅에서는 Compose가 적용된 AndroidLibraryComposeConventionPlugin을 만들 예정입니다.&lt;br&gt;&lt;br&gt;이번 포스팅의 결과물은 아래 Github Repository의 7-AndroidLibraryConventionPlugin 브랜치를 확인하시면 됩니다.&lt;/p&gt;&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;GitHub - taein8935/multi-module-template-aos&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;Contribute to taein8935/multi-module-template-aos development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/taein8935/multi-module-template-aos/tree/7-AndroidLibraryConventionPlugin&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/m7RtO/hyWKJtpEuB/EnYMdMt7BB1LCR3FaKHgG1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot; data-og-url=&quot;https://github.com/taein8935/multi-module-template-aos&quot;&gt;&lt;a href=&quot;https://github.com/taein8935/multi-module-template-aos&quot; target=&quot;_blank&quot; data-source-url=&quot;https://github.com/taein8935/multi-module-template-aos/tree/7-AndroidLibraryConventionPlugin&quot;&gt;&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/m7RtO/hyWKJtpEuB/EnYMdMt7BB1LCR3FaKHgG1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600')&quot;&gt; &lt;/div&gt;&lt;div class=&quot;og-text&quot;&gt;&lt;p class=&quot;og-title&quot;&gt;GitHub - taein8935/multi-module-template-aos&lt;/p&gt;&lt;p class=&quot;og-desc&quot;&gt;Contribute to taein8935/multi-module-template-aos development by creating an account on GitHub.&lt;/p&gt;&lt;p class=&quot;og-host&quot;&gt;github.com&lt;/p&gt;&lt;/div&gt;&lt;/a&gt;&lt;/figure&gt;</description>
      <category>안드로이드/멀티 모듈</category>
      <category>Gradle</category>
      <category>multi module</category>
      <category>멀티 모듈</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/61</guid>
      <comments>https://code-tales.tistory.com/61#entry61comment</comments>
      <pubDate>Mon, 12 Aug 2024 11:00:26 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 멀티 모듈] 6. AndroidApplicationComposeConventionPlugin 만들기</title>
      <link>https://code-tales.tistory.com/60</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;지난 포스팅에서는 app 모듈의 컨벤션 플러그인 AndroidApplicationConventionPlugin을 만들었습니다. 최근 안드로이드에서는 기존의 XML방식에서 선언형 UI 프로그래밍인 Compose 방식으로 많이 전환되고 있는데, &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Compose로 개발을 하기 위해서 빌드 관련 코드를 추가&lt;/b&gt;&lt;/span&gt;해야 합니다. 이번 포스팅에서는 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Compose Application 모듈을 위한 AndroidApplicationComposeConventionPlugin&lt;/b&gt;&lt;/span&gt;을 만들겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;417&quot; data-origin-height=&quot;305&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dg5JYF/btsIWknzU9K/uGaUXlK896E6kEJmIa3y51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dg5JYF/btsIWknzU9K/uGaUXlK896E6kEJmIa3y51/img.png&quot; data-alt=&quot;이번 포스팅까지 진행한 build-logic 모듈의 파일 및 패키지 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dg5JYF/btsIWknzU9K/uGaUXlK896E6kEJmIa3y51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdg5JYF%2FbtsIWknzU9K%2FuGaUXlK896E6kEJmIa3y51%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;이번 포스팅까지 진행한 build-logic 모듈의 파일 및 패키지 구조&quot; loading=&quot;lazy&quot; width=&quot;580&quot; height=&quot;424&quot; data-origin-width=&quot;417&quot; data-origin-height=&quot;305&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이번 포스팅까지 진행한 build-logic 모듈의 파일 및 패키지 구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. AndroidCompose 유틸 함수 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 포스팅과 마찬가지로 여러 컨벤션 플러그인에서 공통으로 사용할 수 있는 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;AndroidCompose 유틸 함수&lt;/b&gt;&lt;/span&gt;를 만들겠습니다. 해당 함수에서는 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;안드로이드 Compose 관련 빌드 로직을 정의&lt;/b&gt;&lt;/span&gt;합니다. 그리고 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;C&lt;/b&gt;&lt;b&gt;ompose 관련 의존성&lt;/b&gt;&lt;/span&gt;을 추가할 수도 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1722947207974&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// AndroidCompose.kt
internal fun Project.configureAndroidCompose(
    commonExtension: CommonExtension&amp;lt;*, *, *, *, *, *&amp;gt;
) {
    commonExtension.run {
        buildFeatures {
            compose = true
        }

        composeOptions {
            kotlinCompilerExtensionVersion = libs.findVersion(&quot;composeCompiler&quot;).get().toString()
        }

        dependencies {
            val bom = libs.findLibrary(&quot;androidx.compose.bom&quot;).get()
            add(&quot;implementation&quot;, platform(bom))
            add(&quot;androidTestImplementation&quot;, platform(bom))
            add(&quot;debugImplementation&quot;, libs.findLibrary(&quot;androidx.ui.tooling.preview&quot;).get())
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Compose 관련 의존성은 compose-bom으로 관리되는 반면, &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Compose Compiler는 composeOptions 블록에 따로 명시&lt;/b&gt;&lt;/span&gt;를 해야 합니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Kotlin 버전마다 Compose Compiler 버전이 상이하므로 주의&lt;/b&gt;&lt;/span&gt;해야 합니다. 여기서는 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Kotlin 1.9.22 버전&lt;/b&gt;&lt;/span&gt;을 사용하기 때문에 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Compose Compiler의 버전은 1.5.9&lt;/b&gt;&lt;/span&gt;로 적용해야 합니다.&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;최근 Kotlin 2.0 버전&lt;/b&gt;&lt;/span&gt;이 출시됐는데, &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Kotlin 2.0 이상에서는 코틀린 버전별로 Compose Compiler를 따로 관리할 필요가 없습니다&lt;/b&gt;&lt;/span&gt;.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. AndroidApplicationComposeConventionPlugin 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 포스팅에서 AndroidApplicationConventionPlugin을 만들었기 때문에 간단하게 구성할 수 있습니다. 아래 코드처럼 플러그인을 적용할 수 있는 pluginManager를 통해서 AndroidApplicationConventionPlugin을 적용하고, 위의 1번 과정에서 만든 유틸 함수를 호출하면 완성입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1722947513877&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// AndroidApplicationComposeConventionPlugin.kt
class AndroidApplicationComposeConventionPlugin: Plugin&amp;lt;Project&amp;gt; {
    override fun apply(target: Project) {
        target.run {
            pluginManager.apply(&quot;multi.module.android.application&quot;)

            val extension = extensions.getByType&amp;lt;ApplicationExtension&amp;gt;()
            configureAndroidCompose(extension)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. AndroidApplicationComposeConventionPlugin 적용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Gradle에게 특정 id로 컨벤션 플러그인을 인식&lt;/b&gt;&lt;/span&gt;할 수 있도록 아래처럼 libs.versions.toml 파일에 id를 정의하고 build-logic 모듈에 위에서 만든 컨벤션 플러그인을 등록합니다. implementationClass에는 Class 이름을 적으면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1722947724348&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// libs.versions.toml
...

# Custom Convention Plugin
multi-module-android-application = { id = &quot;multi.module.android.application&quot;, version = &quot;unspecified&quot;}
multi-module-android-application-compose = { id = &quot;multi.module.android.application.compose&quot;, version = &quot;unspecified&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1722947766668&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// build.gradle.kts (Module :build-logic:convention)
...

gradlePlugin {
   plugins {
      // 이전 포스팅에서 등록한 컨벤션 플러그인
      register(&quot;androidApplication&quot;) {
         id = &quot;multi.module.android.application&quot;
         implementationClass = &quot;AndroidApplicationConventionPlugin&quot;
      }
      
      // 이번 포스팅에서 등록한 컨벤션 플러그인
      register(&quot;androidApplicationCompose&quot;) {
         id = &quot;multi.module.android.application.compose&quot;
         implementationClass = &quot;AndroidApplicationComposeConventionPlugin&quot;
      }
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;마지막으로 build.gradle.kts (:app) 모듈에서 AndroidApplicationComposeConventionPlugin을 적용하고 compose 관련된 코드를 제거하면 됩니다. 아래의 코드를 보면 처음과는 다르게 대부분의 빌드 관련 코드가 컨벤션 플러그인에서 관리되는 모습을 볼 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1722947973621&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// build.gradle.kts (:app)
plugins {
    // 컨벤션 플러그인 적용
    alias(libs.plugins.multi.module.android.application.compose)
}

android {
    namespace = &quot;com.multi.module.template&quot;

    defaultConfig {
        testInstrumentationRunner = &quot;androidx.test.runner.AndroidJUnitRunner&quot;
        vectorDrawables {
            useSupportLibrary = true
        }
    }

    packaging {
        resources {
            excludes += &quot;/META-INF/{AL2.0,LGPL2.1}&quot;
        }
    }
}

dependencies {
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Compose Application 모듈을 위한 AndroidApplicatonComposeConventionPlugin&lt;/b&gt;&lt;/span&gt;을 만들었습니다.&lt;br /&gt;buildTypes, buildFeature 등의 모든 공통 코드를 컨벤션 플러그인에서 관리할 수 있게 되었습니다.&lt;br /&gt;&lt;br /&gt;지금까지는 Application 모듈과 관련된 컨벤션 플러그인을 만들었는데, &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;다음 포스팅에서는 AndroidLibrary 모듈과 관련된 컨벤션 플러그인&lt;/b&gt;&lt;/span&gt;을 만들 예정입니다. 기본적인 구조는 똑같고, 컨벤션 플러그인의 유틸함수도 만들어놓은 상태이기 때문에 추가되는 컨벤션 플러그인들은 쉽게 만들 수 있습니다.&lt;br /&gt;&lt;br /&gt;이번 포스팅의 결과물은 아래 Github Repository의 6-AndroidApplicationComposeConventionPlugin 브랜치를 확인하시면 됩니다.&lt;/p&gt;
&lt;figure id=&quot;og_1722948516075&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - taein8935/multi-module-template-aos&quot; data-og-description=&quot;Contribute to taein8935/multi-module-template-aos development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/taein8935/multi-module-template-aos/tree/6-AndroidApplicationComposeConventionPlugin&quot; data-og-url=&quot;https://github.com/taein8935/multi-module-template-aos&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bgYSd0/hyWKyk6YOZ/zrqflUDC7OGL7AQGJpeHOk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/taein8935/multi-module-template-aos/tree/6-AndroidApplicationComposeConventionPlugin&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/taein8935/multi-module-template-aos/tree/6-AndroidApplicationComposeConventionPlugin&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bgYSd0/hyWKyk6YOZ/zrqflUDC7OGL7AQGJpeHOk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - taein8935/multi-module-template-aos&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to taein8935/multi-module-template-aos development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>안드로이드/멀티 모듈</category>
      <category>Gradle</category>
      <category>multi module</category>
      <category>멀티 모듈</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/60</guid>
      <comments>https://code-tales.tistory.com/60#entry60comment</comments>
      <pubDate>Wed, 7 Aug 2024 11:00:42 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 멀티 모듈] 5. BuildTypes 컨벤션 플러그인에 적용하기</title>
      <link>https://code-tales.tistory.com/59</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;지난 포스팅에서는 AndroidApplicationConventionPlugin을 만들었습니다. 하지만 buildTypes, buildFeatures, composeOptions 등의 코드를 컨벤션 플러그인에 추가하지 않았는데, 이번 포스팅에서는 &lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;buildTypes를 컨벤션 플러그인에서 공통&lt;/span&gt;&lt;/b&gt;으로 사용할 수 있도록 적용하겠습니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;798&quot; data-origin-height=&quot;807&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/O3abD/btsITleZtlF/SThfRZJpAglAS1OMOIOzIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/O3abD/btsITleZtlF/SThfRZJpAglAS1OMOIOzIk/img.png&quot; data-alt=&quot;해당 부분을 컨벤션 플러그인으로 분리하고, build.gradle.kts에서는 제거될 예정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/O3abD/btsITleZtlF/SThfRZJpAglAS1OMOIOzIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FO3abD%2FbtsITleZtlF%2FSThfRZJpAglAS1OMOIOzIk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;해당 부분을 컨벤션 플러그인으로 분리하고, build.gradle.kts에서는 제거될 예정&quot; loading=&quot;lazy&quot; width=&quot;580&quot; height=&quot;587&quot; data-origin-width=&quot;798&quot; data-origin-height=&quot;807&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;해당 부분을 컨벤션 플러그인으로 분리하고, build.gradle.kts에서는 제거될 예정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Application을 위한 BuildTypes 유틸함수 만들기&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;buildTypes를 컨벤션 플러그인에서 공통으로 사용하기 위해서 &lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;build-logic:convention 모듈에 BuildTypes 유틸함수&lt;/span&gt;&lt;/b&gt;를 만들겠습니다. 이전과 같은 패턴으로 Project의 확장함수로 구현하겠습니다.&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// BuildTypes.kt
internal fun Project.configureBuildTypes(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;commonExtension: CommonExtension&amp;lt;*, *, *, *, *, *&amp;gt;,
) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;commonExtension.run {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;buildFeatures {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;buildConfig = true
&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;val apiKey = gradleLocalProperties(rootDir, providers).getProperty(&quot;API_KEY&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;extensions.configure&amp;lt;ApplicationExtension&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;buildTypes {
&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;debug {
&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;// TODO: configure debug build type
&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;&amp;nbsp;&amp;nbsp;create(&quot;staging&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;// TODO: configure staging build type
&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;&amp;nbsp;&amp;nbsp;release {
&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;// TODO: configure release build type
&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;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;컨벤션 플러그인의 유틸 함수로 사용하기 위해서 commonExtension을 인자로 받아서 구현했습니다.&lt;br&gt;API KEY처럼 &lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;보안이 중요한 정보&lt;/span&gt;&lt;/b&gt;는 &lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;localProperties&lt;/span&gt;&lt;/b&gt;에 입력하기 때문에 &lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;gradleLocalProperties 함수&lt;/span&gt;&lt;/b&gt;를 통해 값을 읽어오는 모습을 확인할 수 있습니다.&lt;br&gt;&lt;br&gt;이제 debug, staging, release의 build type을 구현하기 위한 함수를 만들겠습니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Build Variant를 위한 Build Type 구성하기&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;앱을 개발하다 보면 여러 환경에 따라 앱을 다르게 구성해야 할 때가 있는데 테스트용 앱일 수도 있고, 베타버전의 앱일 수도 있습니다. 안드로이드에서는 이러한 다양한 환경에서의 빌드를 하기 위해 &lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;Build Variant&lt;/span&gt;&lt;/b&gt;를 지원합니다. 본 포스팅에서는 debug, staging, release 3가지를 만들겠습니다.&lt;br&gt;&lt;br&gt;Build Variant를 공통으로 관리하기 위해 유틸함수를 구현한 모습입니다.&lt;br&gt;&lt;br&gt;release build type에서는 코드 난독화를 위한 설정도 추가했습니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// BuildTypes.kt
private fun BuildType.configureDebugBuildType(apiKey: String) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;buildConfigField(&quot;String&quot;, &quot;API_KEY&quot;, &quot;\&quot;$apiKey\&quot;&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;buildConfigField(&quot;String&quot;, &quot;BASE_URL&quot;, &quot;\&quot;DEBUG_API_URL\&quot;&quot;)
}

private fun BuildType.configureStagingBuildType(apiKey: String) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;buildConfigField(&quot;String&quot;, &quot;API_KEY&quot;, &quot;\&quot;$apiKey\&quot;&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;buildConfigField(&quot;String&quot;, &quot;BASE_URL&quot;, &quot;\&quot;STAGING_API_URL\&quot;&quot;)
}

private fun BuildType.configureReleaseBuildType(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;commonExtension: CommonExtension&amp;lt;*, *, *, *, *, *&amp;gt;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;apiKey: String
) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;buildConfigField(&quot;String&quot;, &quot;API_KEY&quot;, &quot;\&quot;$apiKey\&quot;&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;buildConfigField(&quot;String&quot;, &quot;BASE_URL&quot;, &quot;\&quot;RELEASE_API_URL\&quot;&quot;)


&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;isMinifyEnabled = true
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;proguardFiles(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;commonExtension.getDefaultProguardFile(&quot;proguard-android-optimize.txt&quot;),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;proguard-rules.pro&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;)
}&lt;/code&gt;&lt;/pre&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// BuildTypes.kt
internal fun Project.configureBuildTypes(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;commonExtension: CommonExtension&amp;lt;*, *, *, *, *, *&amp;gt;,
) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;commonExtension.run {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;buildFeatures {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;buildConfig = true
&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;val apiKey = gradleLocalProperties(rootDir, providers).getProperty(&quot;API_KEY&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;extensions.configure&amp;lt;ApplicationExtension&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;buildTypes {
&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;debug {
&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;configureDebugBuildType(apiKey)
&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;&amp;nbsp;&amp;nbsp;create(&quot;staging&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;configureStagingBuildType(apiKey)
&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;&amp;nbsp;&amp;nbsp;release {
&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;configureReleaseBuildType(commonExtension, apiKey)
&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;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Android Library 모듈을 위한 BuildTypes 적용하기&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;1, 2번 과정은 Application 모듈을 위한 BuildType 적용 방법이었습니다. 안드로이드 프로젝트에서는 &lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;designsystem, data 등의 Android Library 모듈도 많이 사용하므로 Application뿐만 아니라 Android Library의 BuildType도 함께 관리&lt;/span&gt;&lt;/b&gt;해야 합니다. 이를 위한 enum class를 만들겠습니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// ExtensionType.kt
enum class ExtensionType {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;APPLICATION,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;LIBRARY
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;그 후, 1, 2번 과정에서 만든 configureBuildTypes 유틸함수를 수정하겠습니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// BuildTypes.kt
internal fun Project.configureBuildTypes(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;commonExtension: CommonExtension&amp;lt;*, *, *, *, *, *&amp;gt;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;extensionType: ExtensionType
) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;commonExtension.run {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;buildFeatures {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;buildConfig = true
&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;val apiKey = gradleLocalProperties(rootDir, providers).getProperty(&quot;API_KEY&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;when (extensionType) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ExtensionType.APPLICATION -&amp;gt; {
&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;extensions.configure&amp;lt;ApplicationExtension&amp;gt; {
&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;buildTypes {
&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;debug {
&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;configureDebugBuildType(apiKey)
&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;&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;create(&quot;staging&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;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;configureStagingBuildType(apiKey)
&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;&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;release {
&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;configureReleaseBuildType(commonExtension, apiKey)
&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;&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;}
&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;ExtensionType.LIBRARY -&amp;gt; { 
&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;extensions.configure&amp;lt;LibraryExtension&amp;gt; {
&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;buildTypes {
&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;debug {
&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;configureDebugBuildType(apiKey)
&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;&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;create(&quot;staging&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;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;configureStagingBuildType(apiKey)
&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;&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;release {
&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;configureReleaseBuildType(commonExtension, apiKey)
&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;&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;}
&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;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;configureBuildTypes() 함수의 인자로 &lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;ExtensionType을 넘겨받도록 구현&lt;/span&gt;&lt;/b&gt;했습니다. 그리고 &lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;when 조건문을 통해 Application과 Android Library 모듈의 분기처리&lt;/span&gt;&lt;/b&gt;를 해주었고, &lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;각각 다른 Extension을 불러와서 buildTypes를 구성&lt;/span&gt;&lt;/b&gt;해 주었습니다. 이렇게 Application과 Android Library 모듈의 buildTypes를 한 곳에서 구성할 수 있습니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. AndroidApplicationConventionPlugin에 적용하기&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;BuildTypes를 관리하는 유틸 함수를 만들었으니 이전 포스팅에서 만든 AndroidApplicationConventionPlugin에 적용하겠습니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// AndroidApplicationConventionPlugin.kt
class AndroidApplicationConventionPlugin: Plugin&amp;lt;Project&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;override fun apply(target: Project) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;target.run {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;pluginManager.run {
&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;apply(&quot;com.android.application&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;apply(&quot;org.jetbrains.kotlin.android&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;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;extensions.configure&amp;lt;ApplicationExtension&amp;gt; {
&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;defaultConfig {
&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;applicationId = libs.findVersion(&quot;projectApplicationId&quot;).get().toString()
&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;targetSdk = libs.findVersion(&quot;projectTargetSdkVersion&quot;).get().toString().toInt()
&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;versionCode = libs.findVersion(&quot;projectVersionCode&quot;).get().toString().toInt()
&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;versionName = libs.findVersion(&quot;projectVersionName&quot;).get().toString()
&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;&amp;nbsp;&amp;nbsp;configureKotlinAndroid(this)

&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;configureBuildTypes(
&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;commonExtension = this,
&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;extensionType = ExtensionType.APPLICATION
&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;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;extensions.configure 블록 내부에 configureBuildTypes() 함수를 호출하는 모습을 볼 수 있습니다.&lt;br&gt;Application 모듈이기 때문에 extensionType의 인자로는 ExtensionType.APPLICATION을 전달했습니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Build.Gradle.kts (:app)에서 buildTypes 제거하기&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;컨벤션 플러그인에서 buildTypes를 관리하도록 구현했기 때문에 build.gradle.kts (:app)에서는 buildTypes를 제거&lt;/span&gt;&lt;/b&gt;하겠습니다. &lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;추후 생성되는 모듈의 build.gradle.kts 파일에서도 buildTypes를 제거&lt;/span&gt;&lt;/b&gt;할 수 있게 됐습니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;735&quot; data-origin-height=&quot;617&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbVFLy/btsISNiJRY3/gpZR1rVCY36crHU0yiudRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbVFLy/btsISNiJRY3/gpZR1rVCY36crHU0yiudRK/img.png&quot; data-alt=&quot;build.gradle.kts에서 buildTypes가 제거된 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbVFLy/btsISNiJRY3/gpZR1rVCY36crHU0yiudRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbVFLy%2FbtsISNiJRY3%2FgpZR1rVCY36crHU0yiudRK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;build.gradle.kts에서 buildTypes가 제거된 모습&quot; loading=&quot;lazy&quot; width=&quot;580&quot; height=&quot;487&quot; data-origin-width=&quot;735&quot; data-origin-height=&quot;617&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;build.gradle.kts에서 buildTypes가 제거된 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 정리&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;환경에 따라 안드로이드 앱을 다르게 구성해야 할 때 유용하게 사용되는 Build Variant 부분을 컨벤션 모듈의 유틸함수로 만들고, 공통으로 관리할 수 있게 구현했습니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;344&quot; data-origin-height=&quot;126&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxqbOr/btsIThKurtZ/2LFYMRAwkcVqBjvEliWVeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxqbOr/btsIThKurtZ/2LFYMRAwkcVqBjvEliWVeK/img.png&quot; data-alt=&quot;Build Variant 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxqbOr/btsIThKurtZ/2LFYMRAwkcVqBjvEliWVeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxqbOr%2FbtsIThKurtZ%2F2LFYMRAwkcVqBjvEliWVeK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;Build Variant 모습&quot; loading=&quot;lazy&quot; width=&quot;344&quot; height=&quot;126&quot; data-origin-width=&quot;344&quot; data-origin-height=&quot;126&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Build Variant 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;&lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;다음 포스팅에서는 Compose 옵션과 관련된 컨벤션 플러그인&lt;/span&gt;&lt;/b&gt;을 만들어서 buildFeatures와 composeOptions를 공통으로 관리할 수 있게 구현할 예정입니다.&lt;br&gt;&lt;br&gt;이번 포스팅의 결과물은 아래 Github Repository의 5-SettingUpBuildTypes 브랜치를 확인하시면 됩니다.&lt;/p&gt;&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;GitHub - taein8935/multi-module-template-aos&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;Contribute to taein8935/multi-module-template-aos development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/taein8935/multi-module-template-aos/tree/5-SettingUpBuildTypes&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/zNOVz/hyWKxFs1oj/7pkHuqrgxTLO7OXGB7kBQ1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot; data-og-url=&quot;https://github.com/taein8935/multi-module-template-aos&quot;&gt;&lt;a href=&quot;https://github.com/taein8935/multi-module-template-aos&quot; target=&quot;_blank&quot; data-source-url=&quot;https://github.com/taein8935/multi-module-template-aos/tree/5-SettingUpBuildTypes&quot;&gt;&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/zNOVz/hyWKxFs1oj/7pkHuqrgxTLO7OXGB7kBQ1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600')&quot;&gt; &lt;/div&gt;&lt;div class=&quot;og-text&quot;&gt;&lt;p class=&quot;og-title&quot;&gt;GitHub - taein8935/multi-module-template-aos&lt;/p&gt;&lt;p class=&quot;og-desc&quot;&gt;Contribute to taein8935/multi-module-template-aos development by creating an account on GitHub.&lt;/p&gt;&lt;p class=&quot;og-host&quot;&gt;github.com&lt;/p&gt;&lt;/div&gt;&lt;/a&gt;&lt;/figure&gt;</description>
      <category>안드로이드/멀티 모듈</category>
      <category>Gradle</category>
      <category>multi module</category>
      <category>멀티 모듈</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/59</guid>
      <comments>https://code-tales.tistory.com/59#entry59comment</comments>
      <pubDate>Fri, 2 Aug 2024 11:00:47 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 멀티 모듈] 4. AndroidApplicationConventionPlugin 만들기</title>
      <link>https://code-tales.tistory.com/58</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;모듈을 만들었을 때 생성되는 build.gradle.kts 파일의 빌드 관련 중복 코드를 제거하고, 한 곳에서 관리하기 위한 작업을 진행 중입니다. 그래서 지난 포스팅에서는 build-logic:convention 모듈을 만들었습니다. 이번 포스팅에서는 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;AndroidApplicationConventionPlugin&lt;/b&gt;&lt;/span&gt;을 만들고 &lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;build.gradle.kts(Module :app) 모듈에 적용&lt;/span&gt;&lt;/b&gt;하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;330&quot; data-origin-height=&quot;222&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3KaYI/btsIPwCgurK/XDkCxYlXbkyb0gj7Qh3hL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3KaYI/btsIPwCgurK/XDkCxYlXbkyb0gj7Qh3hL1/img.png&quot; data-alt=&quot;이번 포스팅에서 생성한 파일 및 디렉토리 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3KaYI/btsIPwCgurK/XDkCxYlXbkyb0gj7Qh3hL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3KaYI%2FbtsIPwCgurK%2FXDkCxYlXbkyb0gj7Qh3hL1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;이번 포스팅에서 생성한 파일 및 디렉토리 구조&quot; loading=&quot;lazy&quot; width=&quot;580&quot; height=&quot;390&quot; data-origin-width=&quot;330&quot; data-origin-height=&quot;222&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이번 포스팅에서 생성한 파일 및 디렉토리 구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. AndroidApplicationConventionPlugin.kt 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build-logic모듈의 java 패키지 폴더에 AndroidApplicationConventionPlugin.kt을 만듭니다.&lt;/p&gt;
&lt;pre id=&quot;code_1722134240160&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// AndroidApplicationConventionPlugin.kt
import org.gradle.api.Plugin
import org.gradle.api.Project

class AndroidApplicationConventionPlugin: Plugin&amp;lt;Project&amp;gt; {
    override fun apply(target: Project) {
        TODO(&quot;Not yet implemented&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;br /&gt;컨벤션 플러그인을 만들기 위해서는 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;반드시 org.gradle.api.Plugin 인터페이스를 구현해야 합니다.&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;/span&gt;그 후, &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;apply 함수에 빌드 관련 코드&lt;/b&gt;&lt;/span&gt;를 넣어야 합니다.&lt;br /&gt;플러그인을 명시적으로 적용하면 apply 함수가 호출되기 때문입니다.&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;2. AndroidApplicationConventionPlugin.kt 빌드 코드 작성하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AndroidApplicationConventionPlugin을 만들었으니 빌드에 관련된 코드를 컨벤션 플러그인에 작성해야 합니다. 계속 말씀드리지만 컨벤션 플러그인을 만들면 여러 가지 모듈에서 해당 컨벤션 플러그인만 적용하면 중복되는 코드 없이 빌드를 진행할 수 있게 됩니다.&lt;br /&gt;&lt;br /&gt;먼저, 컨벤션 플러그인의 Project 블록 안에서 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;version catalog를 편하게 사용하기 위해서 확장함수&lt;/b&gt;&lt;/span&gt;를 만들어줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1722138338281&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ProjectExt.kt
package com.multi.module.convention

import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalog
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.getByType

val Project.libs: VersionCatalog
    get() = extensions.getByType&amp;lt;VersionCatalogsExtension&amp;gt;().named(&quot;libs&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그리고 아래와 같이 AndroidApplicationConventionPlugin의 apply 함수를 구현합니다.&lt;br /&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;configureKotlinAndroid는 유틸 함수로 만들어서 다른 컨벤션 플러그인에서도 공통으로 사용할 수 있게 구성했습니다.&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;코드를 보면 기존의 build.gradle.kts에 작성된 내용들이 약간은 다른 형태로 구성돼 있는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1722138236549&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// AndroidApplicationConventionPlugin.kt
import com.android.build.api.dsl.ApplicationExtension
import com.multi.module.convention.configureKotlinAndroid
import com.multi.module.convention.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure

class AndroidApplicationConventionPlugin: Plugin&amp;lt;Project&amp;gt; {
    override fun apply(target: Project) {
        target.run {
            pluginManager.run {
                apply(&quot;com.android.application&quot;)
                apply(&quot;org.jetbrains.kotlin.android&quot;)
            }

            extensions.configure&amp;lt;ApplicationExtension&amp;gt; {
                defaultConfig {
                    applicationId = libs.findVersion(&quot;projectApplicationId&quot;).get().toString()
                    targetSdk = libs.findVersion(&quot;projectTargetSdkVersion&quot;).get().toString().toInt()
                    versionCode = libs.findVersion(&quot;projectVersionCode&quot;).get().toString().toInt()
                    versionName = libs.findVersion(&quot;projectVersionName&quot;).get().toString()
                }

                configureKotlinAndroid(this)
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1722138654033&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// libs.versions.toml
[versions]
# Project versions
projectApplicationId = &quot;com.multi.module.template&quot;
projectVersionName = &quot;1.0&quot;
projectVersionCode = &quot;1&quot;
projectMinSdkVersion = &quot;24&quot;
projectTargetSdkVersion = &quot;34&quot;
projectCompileSdkVersion = &quot;34&quot;
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;아래는 configureKotlinAndroid와 configureKotlin 유틸 함수입니다.&lt;br /&gt;유틸 함수로 만든 이유는 다른 컨벤션 플러그인에도 공통으로 들어갈 코드이기 때문입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1722138283671&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Kotlin.kt
package com.multi.module.convention

import com.android.build.api.dsl.CommonExtension
import org.gradle.api.JavaVersion
import org.gradle.api.Project
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

internal fun Project.configureKotlinAndroid(
    commonExtension: CommonExtension&amp;lt;*, *, *, *, *, *&amp;gt;
) {
    commonExtension.apply {
        compileSdk = 34

        defaultConfig.minSdk = 24

        compileOptions {
            isCoreLibraryDesugaringEnabled = true
            sourceCompatibility = JavaVersion.VERSION_11
            targetCompatibility = JavaVersion.VERSION_11
        }
    }

    configureKotlin()

    dependencies {
        &quot;coreLibraryDesugaring&quot;(libs.findLibrary(&quot;desugar.jdk.libs&quot;).get())
    }
}

private fun Project.configureKotlin() {
    tasks.withType&amp;lt;KotlinCompile&amp;gt;().configureEach {
        kotlinOptions {
            jvmTarget = JavaVersion.VERSION_11.toString()
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1722139181296&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// libs.versions.toml
[versions]
...
desugar_jdk_libs = &quot;2.0.4&quot;

[libraries]
...
desugar-jdk-libs = { module = &quot;com.android.tools:desugar_jdk_libs&quot;, version.ref = &quot;desugar_jdk_libs&quot; }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Java 11을 사용하는 API 중에서 안드로이드 API 최소 기준이 높은 경우가 있는데, &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Desugaring&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;은&amp;nbsp;&lt;/span&gt;26 버전이 필요한 API를 21의 버전에서도 사용할 수 있게해 줍니다.&lt;/b&gt;&lt;/span&gt; 대표적으로는 LocalDateTIme, LocalDate, ZondedDateTime과 같은 유틸리티 함수들이 있습니다.&lt;br /&gt;&lt;br /&gt;이렇게 AndroidApplicationConventionPlugin을 만들었는데 어떻게 적용할 수 있을까요?&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. AndroidApplicationConventionPlugin 적용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Gradle에게 특정 id로 컨벤션 플러그인을 인식&lt;/b&gt;&lt;/span&gt;할 수 있도록 알려야 합니다.&lt;br /&gt;libs.versions.toml에 id를 정의하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1722139595472&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// libs.versions.toml
...

[plugins]
...

# Custom Convention Plugin
multi-module-android-application = { id = &quot;multi.module.android.application&quot;, version = &quot;unspecified&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그리고 build-logic 모듈에 모든 컨벤션 플러그인을 등록해야 합니다. 아래처럼 build.gradle.kts (:build-logic:convention) 파일의 하단에 컨벤션 플러그인을 등록해 줍니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;이때 입력하는 id는 libs.versions.toml 파일에 작성한 id와 반드시 동일해야 합니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1722139749848&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// build.gradle.kts (Module :build-logic:convention)
...

gradlePlugin {
   plugins {
      register(&quot;androidApplication&quot;) {
         id = &quot;multi.module.android.application&quot;
         implementationClass = &quot;AndroidApplicationConventionPlugin&quot;
      }
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;마지막으로 build.gradle.kts (:app) 모듈에 컨벤션 플러그인을 적용하면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1722139886069&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// build.gradle.kts (:app)
plugins {
    // 컨벤션 플러그인 적용
    alias(libs.plugins.multi.module.android.application)
}

android {
    namespace = &quot;com.multi.module.template&quot;

    defaultConfig {
        testInstrumentationRunner = &quot;androidx.test.runner.AndroidJUnitRunner&quot;
        vectorDrawables {
            useSupportLibrary = true
        }
    }

    // 다른 컨벤션 플러그인을 만들면서 공통으로 관리될 예정
    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile(&quot;proguard-android-optimize.txt&quot;),
                &quot;proguard-rules.pro&quot;
            )
        }
    }

    // 다른 컨벤션 플러그인을 만들면서 공통으로 관리될 예정
    buildFeatures {
        compose = true
    }
    
    // 다른 컨벤션 플러그인을 만들면서 공통으로 관리될 예정
    composeOptions {
        kotlinCompilerExtensionVersion = &quot;1.5.1&quot;
    }
    
    packaging {
        resources {
            excludes += &quot;/META-INF/{AL2.0,LGPL2.1}&quot;
        }
    }
}

dependencies {
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;코드를 살펴보면 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;applicationId, targetSdk, versionCode, versionName, compileOptions, kotlinOptions 등이 제거&lt;/b&gt;&lt;/span&gt;된 것을 확인할 수 있습니다. 해당 내용은 컨벤션 플러그인에 적용했기 때문입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 AndroidApplicationConventionPlugin을 만들고 적용했습니다.&lt;br /&gt;&lt;br /&gt;XML 기반의 프로젝트인 경우, 해당 플러그인을 app 모듈에 적용할 수 있지만, &lt;br /&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Compose 기반의 프로젝트라면 AndroidApplicationComposeConventionPlugin을 만들어야합니다.&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;그리고 모듈에서 공통으로 사용되는&amp;nbsp; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;buildType,&amp;nbsp;&lt;/span&gt;buildFeature, composeOptions 관련 코드가 남아있는데, 이어지는 포스팅에서 다른 컨벤션 플러그인을 만들면서 제거할 예정입니다.&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;멀티 모듈 아키텍처를 구성하면 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;모듈을 만들 때마다 build.gradle.kts이 생기므로, 컨벤션 플러그인을 만들고 적용해 주는 테크닉이 필요&lt;/b&gt;&lt;/span&gt;합니다. 이렇게 하지 않으면 수많은 중복 코드가 발생할 수 있기 때문입니다.&lt;br /&gt;&lt;br /&gt;이번 포스팅의 결과물은 아래 Github Repository의 4-AndroidApplicationConventionPlugin 브랜치를 확인하시면 됩니다. &lt;/p&gt;
&lt;figure id=&quot;og_1722141603689&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - taein8935/multi-module-template-aos&quot; data-og-description=&quot;Contribute to taein8935/multi-module-template-aos development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/taein8935/multi-module-template-aos/tree/4-AndroidApplicationConventionPlugin&quot; data-og-url=&quot;https://github.com/taein8935/multi-module-template-aos&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ew5EOO/hyWCz6F4nK/HagF4sNtY9ONKo6w9B7AS0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/taein8935/multi-module-template-aos/tree/4-AndroidApplicationConventionPlugin&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/taein8935/multi-module-template-aos/tree/4-AndroidApplicationConventionPlugin&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ew5EOO/hyWCz6F4nK/HagF4sNtY9ONKo6w9B7AS0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - taein8935/multi-module-template-aos&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to taein8935/multi-module-template-aos development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>안드로이드/멀티 모듈</category>
      <category>Gradle</category>
      <category>multi module</category>
      <category>멀티 모듈</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/58</guid>
      <comments>https://code-tales.tistory.com/58#entry58comment</comments>
      <pubDate>Thu, 1 Aug 2024 11:00:40 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 멀티 모듈] 3. build-logic 모듈 만들기</title>
      <link>https://code-tales.tistory.com/57</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이전 포스팅에서 4개의 모듈을 만들었는데 각각 build.gradle.kts 파일이 생성되었고, 아래처럼 빌드와 관련된 중복 코드가 증가했습니다. 앱이 커질수록 모듈과 중복 코드는 계속 증가할 것입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1722075120767&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// build.gradle.kts (Module :home:data)
plugins {
    alias(libs.plugins.android.library)
    alias(libs.plugins.jetbrains.kotlin.android)
}

android {
    namespace = &quot;com.multi.module.home.data&quot;
    compileSdk = 34
    ...
}
...&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1722082352332&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// build.gradle.kts (Module :home:presentation)
plugins {
    alias(libs.plugins.android.library)
    alias(libs.plugins.jetbrains.kotlin.android)
}

android {
    namespace = &quot;com.multi.module.home.presentation&quot;
    compileSdk = 34
    ...
}
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;중복코드를 제거하고 한 곳에서 관리하기 위한 방법은 몇 가지가 존재하는데, 최근에는 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;build-logic&lt;/b&gt;&lt;/span&gt; 모듈을 만들어서 관리하고 있습니다. 먼저 build-logic을 세팅하겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. build-logic 모듈 생성하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;File -&amp;gt; New -&amp;gt; New Module을 선택한 후, Java or Kotlin Library로 아래처럼 build-logic 모듈을 만들어줍니다.&lt;br /&gt;build-logic 모듈에는 Custom Plugin을 만들어야 하는데 Convention Plugin이라고 표현합니다. &lt;br /&gt;그래서 모듈을&amp;nbsp;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;build-logic:convention&lt;/b&gt;&lt;/span&gt;로 생성했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1389&quot; data-origin-height=&quot;972&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FocO4/btsIOJIMYaY/SLwGw6o1e2CIVAfvD2woek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FocO4/btsIOJIMYaY/SLwGw6o1e2CIVAfvD2woek/img.png&quot; data-alt=&quot;build-logic 모듈을 생성하는 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FocO4/btsIOJIMYaY/SLwGw6o1e2CIVAfvD2woek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFocO4%2FbtsIOJIMYaY%2FSLwGw6o1e2CIVAfvD2woek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;build-logic 모듈을 생성하는 모습&quot; loading=&quot;lazy&quot; width=&quot;580&quot; height=&quot;406&quot; data-origin-width=&quot;1389&quot; data-origin-height=&quot;972&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;build-logic 모듈을 생성하는 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;build-logic 모듈을 생성했다면 settings.gradle.kts 파일에 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;includeBuild(&quot;build-logic&quot;)&lt;/b&gt;&lt;/span&gt;을 작성해야 합니다. &lt;br /&gt;왜냐하면 안드로이드 스튜디오 또는 Gradle에서는 일반 Kotlin Module과 Build Module을 구별할 수 없기 때문입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1722078501378&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// settings.gradle.kts (Project Settings)
pluginManagement {

    includeBuild(&quot;build-logic&quot;)
    
    repositories {
        google {
            content {
                includeGroupByRegex(&quot;com\\.android.*&quot;)
                includeGroupByRegex(&quot;com\\.google.*&quot;)
                includeGroupByRegex(&quot;androidx.*&quot;)
            }
        }
        mavenCentral()
        gradlePluginPortal()
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }
}


enableFeaturePreview(&quot;TYPESAFE_PROJECT_ACCESSORS&quot;)

rootProject.name = &quot;MultiModuleTemplate&quot;
include(&quot;:app&quot;)
include(&quot;:home:presentation&quot;)
include(&quot;:home:data&quot;)
include(&quot;:core:presentation:designsystem&quot;)
include(&quot;:home:domain&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;2. build-logic 모듈 설정하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build-logic 빌드와 관련된 설정을 하겠습니다.&lt;br /&gt;&lt;br /&gt;Gradle에게 빌드하는 방법을 제공해 주고, build-logic에서 추가하는 의존성을 다운로드할 원격 저장소 위치를 알려주어야 하므로 build-logic 모듈의 루트 경로에 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;gradle.properties&lt;/b&gt;&lt;/span&gt; 및 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;settings.gradle.kts&lt;/b&gt;&lt;/span&gt; 파일을 생성하고 아래와 같이 입력하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1722079894218&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// build-logic의 gradle.properties
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configureondemand=true&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1722079909728&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// build-logic의 settings.gradle.kts  
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
    }

    versionCatalogs {
        create(&quot;libs&quot;) {
            from(files(&quot;../gradle/libs.versions.toml&quot;))
        }
    }
}

rootProject.name = &quot;build-logic&quot;
include(&quot;:convention&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;build-logic 모듈에 생성한 settings.gradle.kts를 보면 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;rootProject 이름을 'build-logic'&lt;/b&gt;&lt;/span&gt;으로 지정해 주고 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;convention 모듈을 포함&lt;/b&gt;&lt;/span&gt;하도록 설정했습니다. 그리고 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;versionCatalogs&lt;/b&gt;&lt;/span&gt; 파일의 경로를 입력했습니다.&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;3. build-logic:convention 모듈 설정하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;build-logic:convention&lt;/b&gt;&lt;/span&gt; 모듈에 대한 설정을 진행하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1722081674658&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// build.gradle.kts (Module :build-logic:convention)
plugins {
   `kotlin-dsl`
}

group = &quot;com.multi.module.buildlogic&quot;


dependencies {
   // versionCatalogs에 관한 정보는 하단의 git 링크를 통해 확인해주세요.
   compileOnly(libs.android.gradlePlugin)
   compileOnly(libs.android.tools.common)
   compileOnly(libs.kotlin.gradlePlugin)
   compileOnly(libs.ksp.gradlePlugin)
   compileOnly(libs.room.gradlePlugin)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;convention 모듈에서는 Kotlin DSL을 사용해서 Convention Plugin을 만들기 때문에 플러그인데 kotlin-dsl을 추가해야 합니다. 그리고 안드로이드, 코틀린, ksp, room 등과 같은 기능을 사용하기 위해 플러그인과 관련된 의존성을 추가했습니다.&lt;br /&gt;&lt;br /&gt;build-logic 모듈에서 생성하는 모든 플러그인은 컴파일 중에만 관련이 있고 런타임 중에는 아무것도 하지 않기 때문에 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;implementation이 아닌 compileOnly&lt;/b&gt;&lt;/span&gt;를 사용했습니다.&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;4. 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 build-logic:convention 모듈을 만들어서 build-logic과 convention 모듈을 설정했습니다.&lt;br /&gt;&lt;br /&gt;다음 포스팅에서는 모듈을 생성할 때마다 생기는 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;build.gradle.kts 파일의 중복코드를 제거&lt;/b&gt;&lt;/span&gt;하기 위해서 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Custom Convention Plugin&lt;/b&gt;&lt;/span&gt;을 만들겠습니다.&lt;br /&gt;&lt;br /&gt;다소 어렵고 난해한 부분이 있었는데 Gradle을 처음 접하는 분들이라면 혼란스러운 게 지극히 정상적인 일이라고 생각이 들고, Gradle 설정과 관련해서 시간을 많이 소요하지 않았으면 좋겠습니다. 나중에 자연스럽게 이해될 수 있기 때문입니다.&lt;br /&gt;&lt;br /&gt;이번 포스팅의 결과물은 아래 Github Repository의 3-SettingUpBuildLogic 브랜치를 확인하시면 됩니다.&lt;/p&gt;
&lt;figure id=&quot;og_1722350345245&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - taein8935/multi-module-template-aos&quot; data-og-description=&quot;Contribute to taein8935/multi-module-template-aos development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/taein8935/multi-module-template-aos/tree/3-SettingUpBuildLogic&quot; data-og-url=&quot;https://github.com/taein8935/multi-module-template-aos&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://github.com/taein8935/multi-module-template-aos/tree/3-SettingUpBuildLogic&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/taein8935/multi-module-template-aos/tree/3-SettingUpBuildLogic&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - taein8935/multi-module-template-aos&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to taein8935/multi-module-template-aos development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>안드로이드/멀티 모듈</category>
      <category>Gradle</category>
      <category>multi module</category>
      <category>멀티 모듈</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/57</guid>
      <comments>https://code-tales.tistory.com/57#entry57comment</comments>
      <pubDate>Wed, 31 Jul 2024 11:00:02 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 멀티 모듈] 2. 모듈 생성 후 관계 정의하기</title>
      <link>https://code-tales.tistory.com/56</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 안드로이드 멀티 모듈 아키텍처를 구성하기 위해 모듈을 만들겠습니다. 저는 모듈을 &lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;app,&lt;/span&gt;&lt;/b&gt; &lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;home:presentation, home:domain, home:data, core:presentation:designsystem로&lt;/span&gt;&lt;/b&gt; 구성할 것입니다. app 모듈은 자동으로 생성되므로 나머지 모듈을 만들겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Android Library와 관련된 모듈 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈이 안드로이드에 의존적인 경우, 모듈을 만들 때 Android Library를 선택하고 만들면 됩니다. &lt;br /&gt;여기서는 아래의 모듈들이 Android Library에 해당됩니다.&lt;br /&gt;같은 방법으로 아래의 모듈을 만들겠습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;home:presentation&lt;/li&gt;
&lt;li&gt;home:data&lt;/li&gt;
&lt;li&gt;core:presentation:designsystem&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1381&quot; data-origin-height=&quot;982&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SinLU/btsIPlU7Lhs/yHo74KHUY4ZnigeOozKpKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SinLU/btsIPlU7Lhs/yHo74KHUY4ZnigeOozKpKk/img.png&quot; data-alt=&quot;home:data 모듈을 만드는 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SinLU/btsIPlU7Lhs/yHo74KHUY4ZnigeOozKpKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSinLU%2FbtsIPlU7Lhs%2FyHo74KHUY4ZnigeOozKpKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;home:data 모듈을 만드는 모습&quot; loading=&quot;lazy&quot; width=&quot;580&quot; height=&quot;412&quot; data-origin-width=&quot;1381&quot; data-origin-height=&quot;982&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;home:data 모듈을 만드는 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Java or Kotlin Library와 관련된 모듈 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;domain 모듈의 경우 비즈니스 로직을 구현하는 모듈입니다. 그래서 안드로이드에 의존적이지 않고, 순수 Java or Kotlin Library로 생성하는 경우가 많습니다. 여기서도 domain 모듈은 Java or Kotlin Library로 만들겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1385&quot; data-origin-height=&quot;981&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nuqaz/btsIQxz6g0j/MyCPh5YKGAcg922H55L2zk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nuqaz/btsIQxz6g0j/MyCPh5YKGAcg922H55L2zk/img.png&quot; data-alt=&quot;home:domain 모듈을 만드는 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nuqaz/btsIQxz6g0j/MyCPh5YKGAcg922H55L2zk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnuqaz%2FbtsIQxz6g0j%2FMyCPh5YKGAcg922H55L2zk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;home:domain 모듈을 만드는 모습&quot; loading=&quot;lazy&quot; width=&quot;580&quot; height=&quot;411&quot; data-origin-width=&quot;1385&quot; data-origin-height=&quot;981&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;home:domain 모듈을 만드는 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 생성된 모듈 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 생성된 모듈을 확인해 보겠습니다. &lt;br /&gt;정상적으로 생성됐다면 아래와 같이 프로젝트 디렉토리에 모듈이 생성된 모습을 확인할 수 있습니다.&lt;br /&gt;추가로, &lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;각 모듈마다 build.gradle.kts 파일도 생성&lt;/span&gt;&lt;/b&gt;이 되었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;717&quot; data-origin-height=&quot;1039&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boKpzm/btsIQJ1oZda/Ej4PlijwMbUj22jGkEkRA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boKpzm/btsIQJ1oZda/Ej4PlijwMbUj22jGkEkRA0/img.png&quot; data-alt=&quot;모듈이 생성된 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boKpzm/btsIQJ1oZda/Ej4PlijwMbUj22jGkEkRA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboKpzm%2FbtsIQJ1oZda%2FEj4PlijwMbUj22jGkEkRA0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;모듈이 생성된 모습&quot; loading=&quot;lazy&quot; width=&quot;346&quot; height=&quot;501&quot; data-origin-width=&quot;717&quot; data-origin-height=&quot;1039&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;모듈이 생성된 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;멀티모듈 프로젝트에서는 &lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;build.gradle.kts 관리&lt;/span&gt;&lt;/b&gt;가 중요합니다. &lt;br /&gt;왜냐하면 모듈을 생성할 때마다 build.gradle.kts가 생성되므로 중복되는 코드가 지속적으로 발생하기 때문입니다.&lt;br /&gt;&lt;br /&gt;만약, targetSdk를 올려야 하는 상황이라면 Android Library로 생성한 모든 모듈의 build.gradle.kts 파일들을 수정해줘야 합니다. 앱이 커질수록 반복 작업이 많아지는 상황이므로 &lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;이를 통합하여 한 곳에서 관리하는 작업을 추가로 진행해야 합니다.&lt;/span&gt;&lt;/b&gt; 내용이 길어질 수 있어서 다음 포스팅에서 다룰 예정입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 모듈 관계 정의&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈을 만들었으니 관계를 정의해야 합니다. 어떤 모듈이 어떤 모듈을 의존할지에 대한 설정입니다. 제가 정의한 관계는 아래와 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;home:presentation -&amp;gt; home:domain&lt;/li&gt;
&lt;li&gt;home:presentation -&amp;gt; core:presentation:designsystem&lt;/li&gt;
&lt;li&gt;home:data -&amp;gt; home:domain&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 모듈을 type-safe 하게 참조하고 싶다면 settings.gradle.kts 파일에 아래 코드를 삽입하면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;enableFeaturePreview(&quot;TYPESAFE_PROJECT_ACCESSORS&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그리고 아래와 같이 각 모듈의 build.gradle.kts 파일의 dependencies 블록에 implementation 함수를 사용하여 모듈을 참조할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// buidl.gradle.kts (Module :home:presentation)
...
dependencies {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;implementation(projects.home.domain)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;implementation(projects.core.presentation.designsystem)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 멀티 모듈 아키텍처를 만들기 위한 기본적인 모듈 4가지를 만들었습니다. 제가 만든 home 모듈은 home 화면과 관련된 코드가 작성될 예정입니다. 만약, 팀원이 profile과 관련된 개발을 맡는다면 profile:presentation, profile:domain, profile:data 모듈을 만들어서 진행하면 됩니다. 즉, 서로 독립된 환경에서 개발을 진행할 수 있게 됩니다.&lt;br /&gt;&lt;br /&gt;하지만 &lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;모듈을 만들 때마다 빌드와 관련된 build.gradle.kts 파일도 계속 생성&lt;/span&gt;&lt;/b&gt;되기 때문에 이를 해결해야 합니다. 최근에는 &lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;build-logic이라는&lt;/span&gt;&lt;/b&gt; 모듈을 만들고 &lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;org.gradle.api.Plugin&lt;/span&gt;&lt;/b&gt;를 구현하는 &lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;Custom Plugin&lt;/span&gt;&lt;/b&gt;을 만들어서 관리하고 있습니다. &lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;Convention Plugin&lt;/span&gt;&lt;/b&gt;이라고도 불리는데 다음 포스팅에서는 build.gradle.kts의 중복코드를 제거하기 위해서 Custom Plugin을 만들겠습니다.&lt;br /&gt;&lt;br /&gt;이번 포스팅의 결과물은 아래 Github Repository의 2-CreateModule 브랜치를 확인하시면 됩니다.&lt;/p&gt;
&lt;figure id=&quot;og_1722350199457&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - taein8935/multi-module-template-aos&quot; data-og-description=&quot;Contribute to taein8935/multi-module-template-aos development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/taein8935/multi-module-template-aos/tree/2-CreateModule&quot; data-og-url=&quot;https://github.com/taein8935/multi-module-template-aos&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/2fCyu/hyWG1Bcdis/FghklvULjW0K9vdHKqfYIK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/taein8935/multi-module-template-aos/tree/2-CreateModule&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/taein8935/multi-module-template-aos/tree/2-CreateModule&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/2fCyu/hyWG1Bcdis/FghklvULjW0K9vdHKqfYIK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - taein8935/multi-module-template-aos&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to taein8935/multi-module-template-aos development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>안드로이드/멀티 모듈</category>
      <category>Gradle</category>
      <category>multi module</category>
      <category>멀티 모듈</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/56</guid>
      <comments>https://code-tales.tistory.com/56#entry56comment</comments>
      <pubDate>Tue, 30 Jul 2024 11:00:43 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 멀티 모듈] 1. 멀티 모듈 개념과 장단점</title>
      <link>https://code-tales.tistory.com/55</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 멀티 모듈 시리즈에서는 멀티 모듈에 대한 이해와 실제 프로젝트에서 어떻게 세팅하는지에 대해 다룰 예정입니다. 코드 작업을 수행한 경우, 각 포스팅 하단에 Github 링크를 공유할 예정이며, 브랜치를 확인하시면 됩니다.&lt;/p&gt;
&lt;figure id=&quot;og_1722350549527&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - taein8935/multi-module-template-aos&quot; data-og-description=&quot;Contribute to taein8935/multi-module-template-aos development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/taein8935/multi-module-template-aos&quot; data-og-url=&quot;https://github.com/taein8935/multi-module-template-aos&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/co281x/hyWG1VtTZ5/VxvGuZBI7YR5k5bHJxJiFK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/taein8935/multi-module-template-aos&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/taein8935/multi-module-template-aos&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/co281x/hyWG1VtTZ5/VxvGuZBI7YR5k5bHJxJiFK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - taein8935/multi-module-template-aos&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to taein8935/multi-module-template-aos development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;먼저 멀티 모듈을 이해하기 전에 모듈이 무엇인지 살펴보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 모듈이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈은 간단히 말해서 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;라이브러리&lt;/b&gt;&lt;/span&gt;라고 생각하면 됩니다. &lt;br /&gt;프로젝트에 추가한 모든 라이브러리(의존성) 또한 모듈입니다.&lt;br /&gt;즉, &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;특정한 목적을 수행하는 코드가 격리된 컨테이너&lt;/b&gt;&lt;/span&gt;라고 생각할 수 있습니다.&lt;br /&gt;&lt;br /&gt;프로젝트 생성 후에 app 수준의 build.gradle 파일의 하단에 있는 dependencies 블록을 확인해 보겠습니다.&lt;br /&gt;dependencies 블록에 선언돼 있는 모든 싱글 라인들이 각 모듈을 우리의 프로젝트에 포함시켰다고 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;563&quot; data-origin-height=&quot;384&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkJzop/btsIOSFmPF8/Lm4pH6dilu6pTDosJxwgHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkJzop/btsIOSFmPF8/Lm4pH6dilu6pTDosJxwgHk/img.png&quot; data-alt=&quot;필요한 모듈을 사용하기 위해 dependencies에 추가&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkJzop/btsIOSFmPF8/Lm4pH6dilu6pTDosJxwgHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkJzop%2FbtsIOSFmPF8%2FLm4pH6dilu6pTDosJxwgHk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;필요한 모듈을 사용하기 위해 dependencies에 추가&quot; loading=&quot;lazy&quot; width=&quot;580&quot; height=&quot;396&quot; data-origin-width=&quot;563&quot; data-origin-height=&quot;384&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;필요한 모듈을 사용하기 위해 dependencies에 추가&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이렇게 필요한 모듈을 추가함으로써 각 모듈에서 제공하는 기능을 사용할 수 있게 됩니다.&lt;br /&gt;그렇다면 포스팅의 주제인 멀티 모듈이란 무엇인지 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 멀티 모듈이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 모듈은 라이브러리라고 배웠습니다. &lt;br /&gt;&lt;br /&gt;기본적으로 안드로이드 스튜디오에서 새 프로젝트를 만든 경우 프로젝트에는 이미 'app'이라는 하나의 모듈이 있습니다.&lt;br /&gt;'app' 모듈이 자동으로 생성되는 이유는 모든 앱에는 어플리케이션의 진입점 역할을 하는 하나의 앱 모듈이 있어야 하기 때문입니다. &lt;br /&gt;&lt;br /&gt;같은 방식으로 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;프로젝트에서 로컬로 사용하는 자체 모듈&lt;/b&gt;&lt;/span&gt;, 즉 라이브러리를 만들 수도 있습니다.&lt;br /&gt;이러한 경우에는 Maven과 같은 저장소에 배포하지 않고, 프로젝트에서 격리된 컨테이너를 사용하는 방식입니다.&lt;br /&gt;Android Library를 선택해서 모듈을 한 개 만들어보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;786&quot; data-origin-height=&quot;556&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yLZuM/btsIOlgX2rm/IhHua6WcxOFzbXX6rHgXu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yLZuM/btsIOlgX2rm/IhHua6WcxOFzbXX6rHgXu1/img.png&quot; data-alt=&quot;File -&amp;amp;gt; New -&amp;amp;gt; New Module을 선택한 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yLZuM/btsIOlgX2rm/IhHua6WcxOFzbXX6rHgXu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyLZuM%2FbtsIOlgX2rm%2FIhHua6WcxOFzbXX6rHgXu1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;580&quot; height=&quot;410&quot; data-origin-width=&quot;786&quot; data-origin-height=&quot;556&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;File -&amp;gt; New -&amp;gt; New Module을 선택한 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;977&quot; data-origin-height=&quot;621&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Z0noq/btsIOmfPv7P/wiCEpTTgaAQ8zI6JaxdKi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Z0noq/btsIOmfPv7P/wiCEpTTgaAQ8zI6JaxdKi1/img.png&quot; data-alt=&quot;mylibrary 모듈이 생성된 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Z0noq/btsIOmfPv7P/wiCEpTTgaAQ8zI6JaxdKi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZ0noq%2FbtsIOmfPv7P%2FwiCEpTTgaAQ8zI6JaxdKi1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;580&quot; height=&quot;369&quot; data-origin-width=&quot;977&quot; data-origin-height=&quot;621&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;mylibrary 모듈이 생성된 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;mylibrary 모듈이 생성되고 실제로 모듈로 인식된 모습을 볼 수 있습니다.&lt;br /&gt;Gradle은 어떻게 mylibrary가 모듈이라는 것을 인식할 수 있었을까요?&lt;br /&gt;&lt;br /&gt;그 이유는 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;settings.gradle.kts 파일의 하단에 include 함수를 사용해서 모듈의 이름 앞에 콜론을 붙여서 참조&lt;/b&gt;&lt;/span&gt;하기 때문입니다. 저희가 추가하는 모든 모듈은 setting.gradle.kts 파일에 나열됩니다.&lt;br /&gt;&lt;br /&gt;이러한 방식으로 data, domain, presentation 등의 모듈을 추가해서 작업하는 아키텍처를 안드로이드에서는 멀티모듈 프로젝트라고 표현합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 멀티 모듈의 장단점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 생각하는 멀티 모듈 아키텍처의 장단점은 아래와 같습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-1. 장점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; background-color: #f6e199;&quot;&gt;&lt;b&gt;관심사를 분리&lt;/b&gt;&lt;/span&gt;할 수 있습니다. 즉, 개발자가 실수로 팀이 설계한 아키텍처에 위배되는 행위를 할 수 없습니다.&lt;br /&gt;예를 들어, Domain 모듈에서 실수로 Presentation 및 Data 모듈에 있는 클래스 등을 참조할 수 없습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;빌드속도가 빨라집니다.&lt;/span&gt;&lt;/b&gt; 변경된 모듈만 빌드하며, 여러 모듈을 병렬로 빌드할 수도 있기 때문입니다.&lt;br /&gt;예를 들어, Presentation 모듈이 아무 모듈도 의존하지 않는다면 Presentation 모듈을 수정했을때 해당 모듈에 대해서만 빌드를 수행합니다. 하지만, Domain 모듈이 변경되면 Domain 모듈에 의존하고있는 Presentation 및 Data 모듈도 빌드가 수행됩니다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;낮은 결합도와 높은 응집력&lt;/b&gt;&lt;/span&gt;으로 구성하는데 도움을 줍니다. 이는 안드로이드뿐만 아니라 모든 소프트웨어 설계의 기본 원칙에 해당됩니다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;협업&lt;/b&gt;&lt;/span&gt;할 때 서로 분리된 환경에서 작업하기 때문에 충돌이 발생할 확률이 줄어듭니다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Dynamic Feature&lt;/b&gt;&lt;/span&gt;를 사용할 수 있습니다. Dynamic Feature는 런타임중에 특정 모듈을 다운로드할 수 있는 기능입니다. 예를 들어, 리소스를 많이 차지하는 특정 모듈이 있는데, 이 기능이 필요하지 않은 사용자는 APK 사이즈만 커질 뿐입니다. 그런데 Dynamic Feature를 구성하면 사용자가 원할때만 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;앱 실행 중에 모듈(Dynamic Feature)을 다운로드할 수 있습니다.&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-2. 단점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;초기에 멀티모듈 환경을 구축하는데 시간이 많이 소요&lt;/b&gt;&lt;b&gt;됩니다.&lt;/b&gt;&lt;/span&gt; 그리고 Gradle 관련 버그가 발생해서 실제 개발보다 환경 구축하는데 시간이 더 소요될 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;모듈을 무분별하게 생성하면 오히려 복잡&lt;/b&gt;&lt;b&gt;해질 수 있습니다.&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 모듈 및 멀티 모듈의 개념에 대해서 설명했습니다. &lt;br /&gt;&lt;br /&gt;최근에는 기능의 분리, 협업 시 코드 충돌을 해결하기 위해서 멀티 모듈 프로젝트로 구성을 하곤 합니다.&lt;br /&gt;&lt;br /&gt;하지만 개인적으로 멀티 모듈은 초기 세팅 및 Gradle 버그를 수정하는데 시간이 많이 소요된다고 생각이 들어서 단일 모듈로 프로젝트를 진행하는 편입니다. 대신, 단일 패키지를 구조를 모듈처럼 만들어서 멀티 모듈 아키텍처로 쉽게 전환할 수 있는 방식으로 개발하고 있습니다.&lt;br /&gt;&lt;br /&gt;다음 포스팅에서는 앱을 개발하기 위한 domain, data 등의 모듈을 생성해보겠습니다.&lt;/p&gt;</description>
      <category>안드로이드/멀티 모듈</category>
      <category>Gradle</category>
      <category>multi module</category>
      <category>멀티 모듈</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/55</guid>
      <comments>https://code-tales.tistory.com/55#entry55comment</comments>
      <pubDate>Mon, 29 Jul 2024 11:00:16 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드] Dynamic Json Deserialization</title>
      <link>https://code-tales.tistory.com/54</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근 API의 Response 형태를 살펴보면 json이 다양한 형태로 응답을 주는 경우도 있습니다. 안드로이드에서 이러한 Dynamic JSON을 효과적으로 다루는 방법에 대해 포스팅하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1721817764695&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 다양한 형태로 오는 json
[
    {
    	&quot;type&quot;: &quot;USER&quot;,
        &quot;name&quot;: &quot;김철수&quot;,
        &quot;age&quot;: 20,
        &quot;birthday&quot;: &quot;20001201&quot;
    },
    {
    	&quot;type&quot;: &quot;GUEST&quot;,
        &quot;nickname&quot;: &quot;닉네임&quot;
    },
	...
]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;먼저 Data Class 정의 방법부터 살펴보겠습니다.&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;1. Dynamic Json을 위한 Data Class 정의&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-1. 모든 필드에 대해 nullable을 지정하는 방법 (Bad Case)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dynamic json 데이터를 처리할 때, 간단하게 생각하면 아래와 같이 모든 필드를 추가하고 nullable로 지정할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1721818070360&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// bad case
data class PersonDto(
    val type: String,
    val name: String?,
    val age: Int?,
    val birthday: String?,
    val nickname: String?
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;하지만 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;응답의 형태가 다양해질수록 data class의 역할은 모호해지고 복잡해질 수 있습니다.&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;어떻게 하면 다형성을 유지하면서 dynamic json을 처리할 수 있을까요?&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-2. Gson의 JsonDeserializer을 사용하는 방법 (Good Case)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Gson의 JsonDeserializer&lt;/b&gt;&lt;/span&gt;를 이용하면 type을 판단해서 적절한 class로 변환하도록 구현할 수 있습니다.&lt;br /&gt;&lt;br /&gt;먼저, &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;sealed class&lt;/b&gt;&lt;/span&gt;를 이용해서 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;각각의 응답 형태에 대한 data class&lt;/b&gt;&lt;/span&gt;를 정의합니다.&lt;br /&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;알지 못하는 type에 대한 예외처리&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;를 하기 위해서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Unknown이라는 서브클래스&lt;/b&gt;&lt;/span&gt;도 정의했습니다. &lt;/p&gt;
&lt;pre id=&quot;code_1721820689322&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sealed class PersonDto {
    data class User(
        val type: String,
        val name: String,
        val age: Int,
        val birthday: String
    ): PersonDto()

    data class Guest(
        val type: String,
        val nickname: String
    ): PersonDto()
    
    data object Unknown: PersonDto()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그 후, type을 검사하여 적절한 서브 클래스로 변환시켜 주는 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Gson의 JsonDeserializer&lt;/b&gt;&lt;/span&gt;을 구현합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1721821061767&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;object PersonDtoDeserializer : JsonDeserializer&amp;lt;PersonDto&amp;gt; {

    private const val TYPE_USER = &quot;User&quot;
    private const val TYPE_GUEST = &quot;Guest&quot;
    
    override fun deserialize(
        json: JsonElement,
        typeOfT: Type,
        context: JsonDeserializationContext
    ): PersonDto = try {
    
        val type = json.asJsonObject.get(&quot;type&quot;).asString

        // 각 cell_type에 따라 적절한 서브클래스를 직접 반환
        when (type) {
            TYPE_USER -&amp;gt; {
                context.deserialize(
                    json,
                    PersonDto.User::class.java
                )
            }

            TYPE_GUEST -&amp;gt; {
                context.deserialize(
                    json,
                    PersonDto.Guest::class.java
                )
            }

            else -&amp;gt; context.deserialize(json, PersonDto.Unknown::class.java)
        }
    } catch (_: Exception) {
        context.deserialize(json, PersonDto.Unknown::class.java)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;마지막으로 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;JsonAdapter 어노테이션&lt;/b&gt;&lt;/span&gt;을 사용해서 PersonDto와 PersonDtoDeserializer를 연결해 주면&lt;br /&gt;데이터를 가져올 때 type에 따라 적절한 객체를 deserialize 해서 가져오게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1721821252757&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@JsonAdapter(PersonDtoDeserializer::class)
sealed class PersonDto {
    data class User(
        val type: String,
        val name: String,
        val age: Int,
        val birthday: String
    ): PersonDto()

    data class Guest(
        val type: String,
        val nickname: String
    ): PersonDto()
    
    data object Unknown: PersonDto()
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Retrofit을 통한 네트워크 통신과의 호환&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Retrofit을 사용할 때도 연동하는 방법은 무척 간단합니다.&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Retrofit의 객체에 GsonConverterFactory&lt;/b&gt;&lt;/span&gt;를 추가하여 별다른 작업 없이 Dynamic Json을 처리할 수 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;위에서 추가한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;JsonAdapter 어노테이션&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;과&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;PersonDtoDeserializer&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;덕분에 type에 맞춰 자동으로 deserialize가 수행되기 때문입니다.&lt;/span&gt; &lt;/p&gt;
&lt;pre id=&quot;code_1721821548738&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun provideRetrofit(client: OkHttpClient): Retrofit {
    return Retrofit.Builder()
        .addCallAdapterFactory(CoroutineCallAdapterFactory())
        .addConverterFactory(GsonConverterFactory.create()) // GsonConverterFactory
        .client(client)
        .baseUrl(BuildConfig.BASE_URL)
        .build()
}

// RetrofitApi
interface RetrofitApi {
    @GET(&quot;endpoint&quot;)
    suspend fun getPersonList(): PersonResponse
}

// PersonResponse.kt
data class PersonResponse(
    val personList: List&amp;lt;PersonDto&amp;gt;
)&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1721822419158&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 사용방법
retrofitApi.getPersonList().personList.forEach { personDto -&amp;gt;
    when (personDto) {
        is PersonDto.User -&amp;gt; {
            // User 타입
        }

        is PersonDto.Guest -&amp;gt; {
            // Guest 타입
        }

        else -&amp;gt; {
            // 정의되지 않은 타입   
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 Gson의 JsonDeserialzer을 사용해 Dynamic Json을 처리하는 방법을 살펴보았습니다.&lt;br /&gt;&lt;br /&gt;다양한 형태의 JSON 응답을 효율적으로 처리하기 위해 다형성을 유지하고, 필요시에는 라이브러리(Moshi 등)를 활용할 수도 있습니다.&lt;/p&gt;</description>
      <category>안드로이드/Util</category>
      <category>deserialization</category>
      <category>dynamic json</category>
      <category>Gson</category>
      <category>jsonadapter</category>
      <category>retrofit</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/54</guid>
      <comments>https://code-tales.tistory.com/54#entry54comment</comments>
      <pubDate>Wed, 24 Jul 2024 22:14:47 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 Jetpack Compose] Custom Splash Activity 만들기</title>
      <link>https://code-tales.tistory.com/52</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 안드로이드 Jetpack Compose 환경에서 Custom Splash Activity를 만들겠습니다. 그리고 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Animatable()&lt;/b&gt;&lt;/span&gt; 함수를 이용하여 스플래시 화면이 서서히 나타나는 효과를 넣겠습니다.&amp;nbsp;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;안드로이드 12 이상부터는 SplashScreen API&lt;/b&gt;&lt;/span&gt;가 추가됐지만 Custom Splash Activity를 별도로 제작한 이유에 대해서도 설명하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;custom_splash_activity.gif&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;650&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k9Hpw/btsGcJLrJbY/knSKJAs5AerOyLUcY3f4a1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k9Hpw/btsGcJLrJbY/knSKJAs5AerOyLUcY3f4a1/img.gif&quot; data-alt=&quot;Custom Splash Activity로 구현한 스플래시 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k9Hpw/btsGcJLrJbY/knSKJAs5AerOyLUcY3f4a1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/k9Hpw/btsGcJLrJbY/knSKJAs5AerOyLUcY3f4a1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;Custom Splash Activity로 구현한 스플래시 화면&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;650&quot; data-filename=&quot;custom_splash_activity.gif&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;650&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Custom Splash Activity로 구현한 스플래시 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 안드로이드 12 이상에서 제공하는 SplashScreen API의 장단점&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 기능을 사용하면 별도의 Activity 없이 스플래시 화면을 구성할 수 있기 때문에 리소스와 시간을 절약할 수 있지만 정해진 규격대로만 설정할 수 있어서 디자이너와 기획자의 요구사항에는 부족할 수 있습니다. SplashScreen API의 장단점을 정리하면 아래와 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;SplashScreen API의 장점&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Activity를 별도로 생성하지 않아도 돼서 시간과 리소스를 절약할 수 있습니다.&lt;/li&gt;
&lt;li&gt;SplashScreen API를 사용하면 모든 안드로이드 앱과 동일한 경험을 제공해서 일관성을 유지할 수 있습니다.&lt;/li&gt;
&lt;li&gt;구글에서 공식적으로 지원하는 기능을 사용하는 것이기 때문에 추후 문제가 발생할 확률이 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SplashScreen API의 단점&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SplashScreen을 사용하면 화면 가운데에 이미지 한 개와 백그라운드만 설정할 수 있습니다.&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 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. Custom Splash Activity를 만든 이유&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 진행한 프로젝트는 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;스플래시 화면에서 간단하게 서버와 통신하는 기능이 있었고, 화면 가운데와 하단에 이미지를 배치하는 형태&lt;/b&gt;&lt;/span&gt;로 개발을 해야 해서 SplashScreen API를 사용하지 않고, Custom Splash Activity로 제작하게 되었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. Custom Splash Activity 적용하기&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Custom Splash Activity를 만듭니다.&lt;/li&gt;
&lt;li&gt;안드로이드 기본 SplashScreen에 나타나는 배경색과 이미지를 없애기 위해 themes.xml에 style을 생성합니다.&lt;/li&gt;
&lt;li&gt;AndroidManifest.xml에 Custom Splash Activity를 추가하고 style을 적용해 줍니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 과정을 순서대로 예제 코드와 함께 설명하겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3-1. Custom Splash Activity 만들기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 Splash Activity 소스코드입니다. 해당 화면에서 구현한 기능은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Animatable()&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;함수를 사용하여 1.5초 동안 alpha 값이 0부터 1까지 증가하게 하고, 각 이미지의 alpha 속성에 적용하여 &lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;스플래시 화면에 진입했을 때 이미지가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;서서히 나타나는 효과&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;를 적용했습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;LaunchedEffect 컴포저블&lt;/b&gt;&lt;/span&gt; 내부에서 startActivity() 함수를 사용하여 애니메이션이 끝난 후 MainActivity로 이동하는 기능을 넣었습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@SuppressLint(&quot;CustomSplashScreen&quot;)
@AndroidEntryPoint
class SplashActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            // val viewModel = hiltViewModel&amp;lt;SplashViewModel&amp;gt;() // viewmodel 필요한 경우 사용
            CleanarchitectureexampleTheme {

                StatusBarColor(barColor = Mint40)

                SplashScreen()
            }
        }
    }

    @Preview
    @Composable
    fun SplashScreen(

    ) {
        val alpha = remember {
            Animatable(0f)
        }
        LaunchedEffect(key1 = Unit) {
            alpha.animateTo(
                targetValue = 1f,
                animationSpec = tween(1500)
            )
            delay(2000L)

            Intent(this@SplashActivity, MainActivity::class.java).apply {
                flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
            }.let { intent -&amp;gt;
                startActivity(intent)
            }
        }

        Column(
            modifier = Modifier
                .fillMaxSize()
                .background(Mint40),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {

            val painter = painterResource(R.drawable.splash_screen_logo)

            Column(
                modifier = Modifier.weight(1f),
                horizontalAlignment = Alignment.CenterHorizontally,
                verticalArrangement = Arrangement.Center
            ) {
                Image(
                    modifier = Modifier
                        .width(200.dp)
                        .height(93.dp)
                        .alpha(alpha.value),
                    painter = painter,
                    contentDescription = &quot;Welluga Staff&quot;,
                )
            }

            Image(
                modifier = Modifier
                    .fillMaxWidth()
                    .alpha(alpha.value),
                painter = painterResource(id = R.drawable.splash_screen_bottom),
                contentScale = ContentScale.FillWidth,
                contentDescription = &quot;Welluga Staff&quot;
            )
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3-2. themes.xml에 Splash Activity 전용 style 만들기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;안드로이드 12 이상에서 제공해 주는&lt;/b&gt;&lt;/span&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;SplashScreen에 나타나는 이미지를 없애고, 위에서 만든 Custom Splash Activity의 배경색과&lt;/b&gt;&lt;/span&gt; 동일하게 설정해 줍니다. 왜냐하면 앱을 실행했을 때 어색하지 않게 Custom Splash Activity가 나오게 해야 하기 때문입니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;resources xmlns:tools=&quot;http://schemas.android.com/tools&quot;&amp;gt;

    &amp;lt;style name=&quot;Theme.Cleanarchitectureexample&quot; parent=&quot;android:Theme.Material.Light.NoActionBar&quot; /&amp;gt;

    &amp;lt;!-- 스플래시 스크린 설정 --&amp;gt;
    &amp;lt;style name=&quot;Theme.MySplashScreen&quot; parent=&quot;Theme.Cleanarchitectureexample&quot;&amp;gt;
        &amp;lt;item name=&quot;android:windowBackground&quot;&amp;gt;@color/mint_40&amp;lt;/item&amp;gt;
        &amp;lt;item name=&quot;android:windowSplashScreenAnimatedIcon&quot; tools:targetApi=&quot;s&quot;&amp;gt;@color/mint_40&amp;lt;/item&amp;gt;
    &amp;lt;/style&amp;gt;

&amp;lt;/resources&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3-3. AndroidManifest.xml 파일 설정하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 만든 SplashActivity를 시작 화면으로 설정하고 style을 적용하기 위해서 AndroidManifest.xml 파일을 다음과 같이 수정했습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;manifest xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;&amp;gt;

    &amp;lt;application
        ... &amp;gt;
        
        &amp;lt;activity
            android:name=&quot;.view.splash.SplashActivity&quot;
            android:theme=&quot;@style/Theme.MySplashScreen&quot;
            android:exported=&quot;true&quot;&amp;gt;
            &amp;lt;intent-filter&amp;gt;
                &amp;lt;action android:name=&quot;android.intent.action.MAIN&quot; /&amp;gt;
                &amp;lt;category android:name=&quot;android.intent.category.LAUNCHER&quot; /&amp;gt;
            &amp;lt;/intent-filter&amp;gt;
        &amp;lt;/activity&amp;gt;

        &amp;lt;activity
            android:name=&quot;.view.main.MainActivity&quot;
            android:exported=&quot;true&quot;&amp;gt;
        &amp;lt;/activity&amp;gt;
    &amp;lt;/application&amp;gt;
&amp;lt;/manifest&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 결론&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;안드로이드에서 제공하는 SplashScreen API는 사용이 간편하고 일관된 사용자 경험을 제공&lt;/li&gt;
&lt;li&gt;Custom Splash Activity는 디자이너와 기획자의 요구사항을 충족시키고 더 많은 자유도와 유연성을 가짐&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 Custom Splash Activity를 구현하는 과정에 대해 설명했지만, 여러분의 프로젝트 요구사항에 따라 SplashScreen API, Custom Splash Activity 중에서 적합한 방법을 선택하면 좋을 것 같습니다.&lt;/p&gt;</description>
      <category>안드로이드/Compose</category>
      <category>Jetpack Compose</category>
      <category>Splash</category>
      <category>스플래시</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/52</guid>
      <comments>https://code-tales.tistory.com/52#entry52comment</comments>
      <pubDate>Sun, 31 Mar 2024 11:30:05 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 트러블 슈팅] 웹뷰 JavascriptInterface 난독화 주의할 점</title>
      <link>https://code-tales.tistory.com/50</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;안드로이드 앱에서 웹 콘텐츠와의 상호작용은 흔히 웹뷰(Webview)를 통해 이루어집니다. 특히 앱과 웹 서버 간의 통신을 위해 &lt;/span&gt;JavascriptInterface&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;를 추가하는 경우가 많습니다. 이번 포스팅에서는 &lt;/span&gt;JavascriptInterface&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;를 사용할 때 난독화 때문에 발생한 트러블 슈팅 경험에 대해 공유하고자 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 문제 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;개발 중인 앱에서는 Firebase Cloud Messaging(FCM) 토큰을 서버에 전송해 푸시 알림을 받을 필요가 있었습니다. 이를 위해 웹뷰를 통해 로그인 시, 안드로이드에서 정의한 로그인 Data Transfer Object(DTO)를 Gson 라이브러리를 이용하여 JSON 문자열로 변환하고, Web Bridge 방식을 통해 데이터를 웹 서버로 전달하는 방식을 사용했습니다. debug 빌드에서는 이 과정이 문제없이 작동했지만, release 빌드에서는 난독화 과정으로 인해 DTO의 프로퍼티들이 &lt;/span&gt;a&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;, &lt;/span&gt;b&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;, &lt;/span&gt;c&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;와 같이 의미 없는 형태로 변환되면서 문제가 발생했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;321&quot; data-origin-height=&quot;266&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n0gex/btsF3n03gE7/08RAwKKEMi6VKAqz5pQKrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n0gex/btsF3n03gE7/08RAwKKEMi6VKAqz5pQKrK/img.png&quot; data-alt=&quot;웹뷰와 브릿지 통신을 하기 위한 DTO&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n0gex/btsF3n03gE7/08RAwKKEMi6VKAqz5pQKrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn0gex%2FbtsF3n03gE7%2F08RAwKKEMi6VKAqz5pQKrK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;웹뷰와 브릿지 통신을 하기 위한 DTO&quot; loading=&quot;lazy&quot; width=&quot;321&quot; height=&quot;266&quot; data-origin-width=&quot;321&quot; data-origin-height=&quot;266&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;웹뷰와 브릿지 통신을 하기 위한 DTO&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 문제 원인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;release 빌드에서 난독화 옵션(isMinifyEnabled)을 활성화하면, proguard 설정이 필요하며 JavascriptInterface에도 적용되어야 하는데 적용하지 않았었습니다. 그리고 난독화 과정에서 중요한 클래스나 메서드, 변수의 이름이 변경되지 않도록 관련 DTO에 @Keep 어노테이션을 붙여야 하는데 붙이지 않은 것을 확인했습니다. 그래서 release 빌드 시, 문제가 발생했다고 가정한 후 하나씩 기능을 적용했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 해결 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드에서 난독화는 보안을 강화하는 동시에 원치 않은 버그를 발생시킬 수 있는데, 특정 클래스나 함수의 난독화를 제외시키기 위해서 Proguard 파일을 추가해 주었고, 통신을 위한 DTO 클래스에도 @Keep 어노테이션을 추가했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-1. JavaInterface 관련 proguard 파일 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JavascriptInterface가 포함된 메서드가 난독화되지 않도록 ProGuard 설정 파일에 예외 규칙을 추가했습니다. 이렇게 하면 release 빌드에서도 웹뷰가 JavaScript를 통해 안드로이드 메서드를 호출할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1711242929583&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-keepattributes JavascriptInterface
-keepattributes *Annotation*

-keepclassmembers class * {
    @android.webkit.JavascriptInterface &amp;lt;methods&amp;gt;;
}

-keepclassmembers class com.company.app.jsBridge.BaseJavascriptInterface {
    public *;
}

-keep public class com.company.app.jsBridge.BaseJavascriptInterface&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-2. gson 관련 proguard 파일 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gson을 사용하여 JSON 문자열을 자바 객체로 변환하거나 그 반대로 변환할 때, 난독화로 인해 필드 이름이 변경되면 안 됩니다. 이를 방지하기 위해 Gson 사용에 필요한 ProGuard 규칙을 별도로 설정했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1711242967502&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;##---------------Begin: proguard configuration for Gson  ----------
# Gson uses generic type information stored in a class file when working with fields. Proguard
# removes such information by default, so configure it to keep all of it.
-keepattributes Signature


# Gson specific classes
-keep class sun.misc.Unsafe { *; }
#-keep class com.google.gson.stream.** { *; }

# Application classes that will be serialized/deserialized over Gson
-keep class com.google.gson.examples.android.model.** { *; }

##---------------End: proguard configuration for Gson  ----------&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-3. @Keep 어노테이션 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스나 메서드를 난독화 과정에서 제외하기 위해 @Keep 어노테이션을 사용했습니다. 이 어노테이션은 ProGuard 또는 R8이 해당 코드를 유지하도록 지시합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;import androidx.annotation.Keep

@Keep
data class AppStaffDevice (
    val appVersion: String,
    val brand: String,
    val fcmToken: String,
    val manufacturer: String,
    val model: String,
    val osVersion: String,
)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;한 가지 주의할 점은 @Keep 어노테이션이 Jetpack 라이브러리뿐만 아니라 다른 라이브러리에도 존재한다는 점입니다. 확인을 하지 않고 import 자동완성 기능을 사용하면 아래와 같이 원치 않은 @Keep 어노테이션이 추가될 수 있으니깐 주의해야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1711243228504&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import com.google.errorprone.annotations.Keep

// 어노테이션의 이름이 동일해서 얼핏보면 문제를 찾기 힘들 수 있음
@Keep
data class AppStaffDevice (
    val appVersion: String,
    val brand: String,
    val fcmToken: String,
    val manufacturer: String,
    val model: String,
    val osVersion: String,
)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;난독화는 앱의 보안을 강화하고 리버스 엔지니어링을 방지하는 중요한 과정입니다. 하지만 난독화 설정을 잘못하면 예기치 않은 오류가 발생할 수 있습니다. 특히 웹뷰와 &lt;/span&gt;JavascriptInterface&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;를 사용하는 경우, ProGuard 설정과 어노테이션의 올바른 사용이 필수적입니다. 이번 이슈를 통해 난독화 설정의 중요성과 관련된 주의 사항을 다시 한번 인식하게 되었고, 이 경험이 다른 개발자분들께도 도움이 되기를 바랍니다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>안드로이드/트러블 슈팅</category>
      <category>안드로이드</category>
      <category>트러블슈팅</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/50</guid>
      <comments>https://code-tales.tistory.com/50#entry50comment</comments>
      <pubDate>Sun, 24 Mar 2024 11:30:26 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 onBackPressedDispatcher] 뒤로 가기 두 번 눌러서 앱 종료하기</title>
      <link>https://code-tales.tistory.com/49</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 앱에서 제일 처음의 화면에서 뒤로 가기를 눌렀을 때 앱이 바로 종료되지 않고, 일정 시간 내에 뒤로 가기 버튼을 한번 더 눌렀을 때 앱이 종료되는 것을 많이 볼 수 있습니다. 이번 포스팅에서는 안드로이드 Jetpack의 OnBackPressedDispatcher를 활용해서 뒤로 가기를 두 번 눌렀을 때 앱이 종료되는 기능을 구현하겠습니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;446&quot; data-origin-height=&quot;926&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWBhVu/btsFUH1x5DU/IpKbskPjzD4lCtRtwxIiR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWBhVu/btsFUH1x5DU/IpKbskPjzD4lCtRtwxIiR0/img.png&quot; data-alt=&quot;뒤로가기 버튼을 한번 눌렀을 때 나오는 토스트 팝업&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWBhVu/btsFUH1x5DU/IpKbskPjzD4lCtRtwxIiR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWBhVu%2FbtsFUH1x5DU%2FIpKbskPjzD4lCtRtwxIiR0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;뒤로가기 버튼을 한번 눌렀을 때 나오는 토스트 팝업&quot; loading=&quot;lazy&quot; width=&quot;281&quot; height=&quot;583&quot; data-origin-width=&quot;446&quot; data-origin-height=&quot;926&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;뒤로가기 버튼을 한번 눌렀을 때 나오는 토스트 팝업&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. OnBackPressedDispatcher란?&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;onBackPressedDispatcher는 안드로이드 Jetpack 컴포넌트 중 하나로, 액티비티 뒤로 가기 버튼의 동작을 관리하고 있습니다. 기존에는 onBackPressed() 함수를 오버라이드해서 뒤로 가기 기능을 커스텀할 수 있었지만, API 33 이후부터 해당 함수가 Deprecated 되면서 onBackPressedDispatcher로 대체되었습니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;646&quot; data-origin-height=&quot;502&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9xxRa/btsFX2Xiawd/S6HAVkX8QHBxQzEaY4Z6A0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9xxRa/btsFX2Xiawd/S6HAVkX8QHBxQzEaY4Z6A0/img.png&quot; data-alt=&quot;API 33이상부터 deprecated된&amp;amp;amp;nbsp; onBackPressed. OnBackPressedDispatcher 사용을 권장한다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9xxRa/btsFX2Xiawd/S6HAVkX8QHBxQzEaY4Z6A0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9xxRa%2FbtsFX2Xiawd%2FS6HAVkX8QHBxQzEaY4Z6A0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;API 33이상부터 deprecated된 onBackPressed 모습&quot; loading=&quot;lazy&quot; width=&quot;646&quot; height=&quot;502&quot; data-origin-width=&quot;646&quot; data-origin-height=&quot;502&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;API 33이상부터 deprecated된&amp;amp;nbsp; onBackPressed. OnBackPressedDispatcher 사용을 권장한다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. OnBackPressedDispatcher를 사용해서 뒤로 가기 구현하기&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;onBackPressedDispatcher를 사용해서 뒤로 가기를 사용하기 위해서는 OnBackPressedCallback의 구현체를 사용해야 합니다. OnBackPressedCallback는 추상클래스로써 직접 구현체를 만들기 도하지만 익명 객체로 생성하여 구현하면 편리합니다.&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1. OnBackPressedCallback 객체 생성하기&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드는 OnBackPressedCallback을 익명 객체로 생성하고, 뒤로 가기를 눌렀을 때 실행될 기능을 handleOnBackPressed() 함수에 구현한 모습입니다. 또한, 익명 객체로 생성 시 OnBackPressedCallback이 사용 가능한지에 대한 변수를 생성자에 true로 전달했습니다.&lt;/p&gt;&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt; 
 &lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;private var backKeyPressedTime = 0L
private val onBackPressedCallback: OnBackPressedCallback = object: OnBackPressedCallback(true) {
    override fun handleOnBackPressed() {
        if (System.currentTimeMillis() &amp;gt; backKeyPressedTime + 2000) {
            backKeyPressedTime = System.currentTimeMillis()
            Toast.makeText(this@MainActivity, &quot;뒤로 버튼을 한번 더 누르시면 종료됩니다.&quot;, Toast.LENGTH_SHORT).show()
        } else {
            finish()
        }
    }
}&lt;/code&gt;&lt;/pre&gt; 
&lt;/div&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;해당 코드는 뒤로 가기 버튼을 한번 누르면 Toast 팝업창을 보여주고, 2초 안에 뒤로 가기를 한번 더 누르면 앱이 종료되는 로직입니다.&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2. OnBackPressedCallback를 OnBackPressedDispatcher에 제공하기&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이제 위에서 만든 onBackPressedCallback 객체를 OnBackPressedDispatcher에 제공하겠습니다. 아래 코드 예제처럼 OnBackPressedDispatcher의 addCallback 함수를 사용해서 콜백 함수를 등록할 수 있습니다. 뒤로 가기 버튼을 눌렀을 때 토스트 팝업창이 나오며 2초 안에 다시 눌렀을 때는 앱이 종료됩니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;class MainActivity : ComponentActivity() {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private var backKeyPressedTime = 0L
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private val onBackPressedCallback: OnBackPressedCallback = object: OnBackPressedCallback(true) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;override fun handleOnBackPressed() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (System.currentTimeMillis() &amp;gt; backKeyPressedTime + 2000) {
&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;backKeyPressedTime = System.currentTimeMillis()

&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;Toast.makeText(this@MainActivity, &quot;뒤로 버튼을 한번 더 누르시면 종료됩니다.&quot;, Toast.LENGTH_SHORT).show()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} else {
&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;finish()
&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;&amp;nbsp;&amp;nbsp;override fun onCreate(savedInstanceState: Bundle?) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;super.onCreate(savedInstanceState)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
		
&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;/code&gt;&lt;/pre&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. OnBackPressedDispatcher 정리&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 뒤로 가기 두 번 눌렀을 때 앱이 종료되는 예제와 함께 OnBackPressedDispatcher의 사용 방법에 대해 살펴보았습니다. 안드로이드 API 33 이상부터 onBackPressed() 함수가 Deprecated 되면서 OnBackPressedDispatcher를 사용해야 합니다. 그리고 예제에서 설명한 기능을 구현하면 사용자의 실수로 인한 앱 종료를 예방하면서 사용자 경험 개선에도 도움이 된다고 생각합니다.&lt;/p&gt;</description>
      <category>안드로이드</category>
      <category>OnBackPressedDispatcher</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/49</guid>
      <comments>https://code-tales.tistory.com/49#entry49comment</comments>
      <pubDate>Sat, 23 Mar 2024 11:30:00 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 트러블 슈팅] 코틀린에서 가변인자(vararg)를 사용할 때 주의할 점</title>
      <link>https://code-tales.tistory.com/48</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;개발을 하다 보면 여러 가지 트러블 슈팅을 겪게 되는데 이번 포스팅에서는 안드로이드 코틀린에서 가변인자(vararg)를 사용할 때 발생한 문제와 주의할 점에 대해 공유하고자 합니다. 코틀린의 vararg 키워드는 함수에 가변 개수의 인자를 전달할 수 있어서 큰 유연성을 제공합니다. 그러나 제대로 알고 사용하지 않는다면, 예상치 못한 결과에 직면할 때가 있는데 vararg를 사용하다가 발생한 문제와 해결 과정에 대해 포스팅하겠습니다.&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 문제 상황&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린에서 vararg를 사용해서 문자열 리소스 ID와 포맷 인자를 받아 로컬라이즈 된 문자열을 반환하는 함수를 따로 구현했었습니다. 제가 개발했던 서비스는 다국어를 지원하고 있었고 앱 내에 언어를 선택하는 기능이 있었기 때문에 Context.getString()을 그대로 사용하지 않고, 래핑 해준 형태로 사용했기 때문입니다. 아래는 예시 코드입니다.&lt;/p&gt;&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt; 
 &lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// MainActivity.kt
@AndroidEntryPoint
class MainActivity : ComponentActivity() {

    fun getStringWrapper(resId: Int, vararg formatArgs: Any?): String {
        return getString(resId, formatArgs)
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
		...
        
        val text = getStringWrapper(R.string.blog_name_format, &quot;개발자가 들려주는 코딩 이야기&quot;)
        Log.d(&quot;test&quot;, &quot;text $text&quot;)
    }
}

// strings.xml
&amp;lt;resources&amp;gt;
    &amp;lt;string name=&quot;blog_name_format&quot;&amp;gt;제 블로그 이름은 %s 입니다.&amp;lt;/string&amp;gt;
&amp;lt;/resources&amp;gt;&lt;/code&gt;&lt;/pre&gt; 
&lt;/div&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;위 함수의 결과로 반환된 text를 로그로 출력했을 때, 문자열이 예상과 다르게 포맷되지 않고 아래와 같이 메모리 주소 형태로 출력되는 것을 볼 수 있었습니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;제 블로그 이름은 [Ljava.lang.Object;@81582a2 입니다.&lt;/code&gt;&lt;/pre&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 문제 원인&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;안드로이드 Context 내에 있는 getString() 함수를 살펴보면 가변 인자를 받도록 되어있습니다.&amp;nbsp;&lt;/span&gt;위 문제의 원인은 getString() 함수에 formatArgs를 그대로 전달했기 때문입니다. 이를 이해하려면 코틀린의 vararg 작동방식에 대해 알고 있어야 합니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;619&quot; data-origin-height=&quot;103&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EuAts/btsFYr3ufzc/MKCMNk14aAO76PMPdO2Mp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EuAts/btsFYr3ufzc/MKCMNk14aAO76PMPdO2Mp1/img.png&quot; data-alt=&quot;안드로이드 Context 내의 getString() 함수&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EuAts/btsFYr3ufzc/MKCMNk14aAO76PMPdO2Mp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEuAts%2FbtsFYr3ufzc%2FMKCMNk14aAO76PMPdO2Mp1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;안드로이드 Context 내의 getString() 함수&quot; loading=&quot;lazy&quot; width=&quot;619&quot; height=&quot;103&quot; data-origin-width=&quot;619&quot; data-origin-height=&quot;103&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;안드로이드 Context 내의 getString() 함수&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;&lt;span style=&quot;color: #333333;&quot;&gt;코틀린에서는 vararg 키워드로 받은 인자를 실제로는 배열로 처리하며, 이 배열을 다른 vararg 매개변수를 받는 함수에 직접 전달하면 배열 자체가 하나의 인자로 전달됩니다. 그래서 formatArgs의 내용이 문자열 포맷의 인자로 사용되지 않고 배열의 toString() 결과가 출력된 것이었습니다.&lt;/span&gt;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;3. 해결 방법&lt;/span&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이 문제를 해결하기 위해서는 가변 인자를 다른 함수에 전달할 때 * 연산자를 사용해야 합니다. 코틀린에서는 포인터 개념이 없기 때문에 * 연산자는 spread operator의 기능을 합니다. *는 배열 앞에 위치하며 배열의 각 요소를 함수의 가변 인자로 전달합니다. 배열을 하나의 객체로 받는 대신 각 요소를 개별 인자로 받아야 할 때 사용할 수 있습니다. 수정된 getStringWrapper 함수는 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@AndroidEntryPoint
class MainActivity : ComponentActivity() {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fun getStringWrapper(resId: Int, vararg formatArgs: Any?): String {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// formatArgs 앞에 * 연산자 추가
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return getString(resId, *formatArgs)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;override fun onCreate(savedInstanceState: Bundle?) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;super.onCreate(savedInstanceState)
&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;val text = getStringWrapper(R.string.blog_name_format, &quot;개발자가 들려주는 코딩 이야기&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Log.d(&quot;test&quot;, &quot;text $text&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;위 코드처럼 formatArgs 앞에 스프레드 연산자 *를 사용해서 가변 인자로 전달을 할 수 있게 되었고 배열의 toString() 결과가 출력되던 로그는 아래와 같이 정상적으로 출력됐습니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;제 블로그 이름은 개발자가 들려주는 코딩 이야기 입니다.&lt;/code&gt;&lt;/pre&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 결론&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 안드로이드 앱 개발 중에 문자열 리소스 ID와 포맷 인자를 받는 함수를 만들면서 발생한 문제와 해결과정 대해 살펴보았습니다. vararg를 받는 함수에 데이터를 전달할 때, 배열이 아닌 가변 인자로써 데이터를 전달하려면 * 연산자를 사용해야 한다는 것을 알게 되었습니다. 이 글이 도움이 되었으면 좋겠습니다.&lt;/p&gt;</description>
      <category>안드로이드/트러블 슈팅</category>
      <category>안드로이드</category>
      <category>코틀린</category>
      <category>트러블슈팅</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/48</guid>
      <comments>https://code-tales.tistory.com/48#entry48comment</comments>
      <pubDate>Fri, 22 Mar 2024 11:30:23 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 Coroutine Flow] 코틀린 Flow에 대해서</title>
      <link>https://code-tales.tistory.com/47</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 앱 개발에 있어서 비동기 프로그래밍은 중요한 부분을 차지합니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;UI가 반응성 있게 동작하도록 하기 위해서 네트워크 요청, 데이터베이스 작업 등 시간이 걸리는 작업을 메인 스레드에서 분리해서 처리하게 됩니다. 이러한 작업을 리액티브 프로그래밍으로 처리하면 코드 관리 측면에서 유리해지는데, 안드로이드 코틀린 Coroutine의 Flow는 데이터 스트림으로써 리액티브 프로그래밍을 지원합니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Kotlin Flow 소개&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린 Flow는 코루틴을 기반으로 한 리액티브 프로그래밍을 가능하게 하며, 시간에 따라 여러 값을 방출할 수 있는 콜드 스트림(cold stream)입니다. 콜드 스트림은 아래와 같은 특징이 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터를 내부에서 생성&lt;/li&gt;
&lt;li&gt;소비자가 구독할 때, 데이터를 생성&lt;/li&gt;
&lt;li&gt;하나의 생산자에 하나의 소비자만 존재&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 코루틴에서 이러한 데이터 스트림을 구현할 때 Flow를 사용하게 되며, 데이터 스트림은 아래의 구성으로 역할이 나뉘게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;937&quot; data-origin-height=&quot;422&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bz7931/btsFU93CXF9/thmZBzRfXfJVKVwrabpaok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bz7931/btsFU93CXF9/thmZBzRfXfJVKVwrabpaok/img.png&quot; data-alt=&quot;Kotlin Flow Data Stream&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bz7931/btsFU93CXF9/thmZBzRfXfJVKVwrabpaok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbz7931%2FbtsFU93CXF9%2FthmZBzRfXfJVKVwrabpaok%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;Kotlin Flow Data Stream&quot; loading=&quot;lazy&quot; width=&quot;937&quot; height=&quot;422&quot; data-origin-width=&quot;937&quot; data-origin-height=&quot;422&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Kotlin Flow Data Stream&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;생산자 (Producer)&lt;/li&gt;
&lt;li&gt;중간 연산자 (Intermediary)&lt;/li&gt;
&lt;li&gt;소비자 (Consumer)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;생산자, 중간 연산자, 소비자 3가지의 구성으로 나뉘게 됩니다. 단어 그대로 생산자는 데이터를 생산하는 역할을 하고 있으며, 중간 연산자는 UI에서 바로 사용할 수 있는 데이터 형태로&lt;/span&gt; 변환해 주는 역할을 합니다. 마지막으로 소비자는 중간 연산자가 전달해 준 데이터를 받아서 UI 등의 작업을 처리하는 형태입니다.&lt;/b&gt; 각각의 역할에 대해 더 자세히 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Consumer (생산자)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생산자는 데이터를 생성하는 역할을 합니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;코루틴 Flow에서는 flow { } 블록을 통해 Flow를 만들 수 있고 블록 내부에서 emit() 함수를 사용하여 데이터를 생성합니다.&lt;/b&gt;&lt;/span&gt; &lt;br /&gt;&lt;br /&gt;생산자가 가져오는 데이터는 보통 DataSource로부터 가져옵니다. 즉, API를 통한 외부 데이터(Remote Data Source)와 DB(Local Data Source) 데이터입니다. &lt;br /&gt;&lt;br /&gt;그런데 이번 포스팅에서는 Flow를 이해하기 위해 간단한 예제로 설명을 진행하겠습니다. 아래 예제는 숫자를 1씩 증가시킨 데이터를 발행하는 코드입니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun getNumberFlow(): Flow&amp;lt;Int&amp;gt; = flow {
    var num = 1
    while (true) {
        emit(num++)
        delay(1000)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;3. Intermediary (중간 연산자)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;생산자가 생성한 데이터를 사용하기 좋게 데이터를 변환하는 역할을 수행합니다.&lt;/b&gt;&lt;/span&gt; 즉, 서버로부터 받은 데이터를 UI에서 사용하는 형태로 변환하여 UI 레이어에서 필요한 데이터만을 가져오기 위함입니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;중간 연산자는 데이터 변형을 위한 map, 데이터 필터링을 위한 filter 등을 많이 사용합니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// 생산자가 생성한 데이터에 *2 연산을 수행
getNumberFlow().map { number -&amp;gt; 
    number * 2
}

// 생산자가 생성한 데이터 중 1 이상의 데이터만 가져옴
getNumberFlow().filter { number -&amp;gt;
    number &amp;gt;= 1
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;4. Consumer (소비자)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 소비자입니다. 소비자는 중간 연산자가 변환한 데이터를 실제로 받는 영역으로써 전달받은 데이터를 소비할 수 있습니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;즉, UI 레이어에서 데이터를 소비하고, 적절한 UI를 표시하는 역할을 합니다. 코루틴을 사용해서 비동기 작업을 실행하는 방법은 GlobalScrope.launch { }를 사용하기도 하지만, 안드로이드에서는 생명주기를 고려해서 lifecycleScope나 viewModelScope를 사용합니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1710853983325&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;viewModelScope.launch { 
    getNumberFlow().collect { number -&amp;gt;
        // 상태 관련 변수 Update
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;네트워크 요청을 통해 데이터를 받아온다면 아래와 같은 형태로 Flow를 사용할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1710853969319&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun fetchUserData(): Flow&amp;lt;User&amp;gt; = flow {
    val user = api.fetchUser() // 네트워크 요청으로 사용자 데이터를 가져옴
    emit(user) // 가져온 데이터를 발행
}.flowOn(Dispatchers.IO) // IO 스레드에서 실행

lifecycleScope.launch {
    fetchUserData().collect { user -&amp;gt;
        // UI 업데이트
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 안드로이드 코틀린 Coroutine과 Flow의 기본적인 사용방법에 대해 알아보았습니다. 리액티브&amp;nbsp;프로그래밍 방식을 통해 비동기 처리를 간단하게 할 수 있으며, 결과적으로 코드의 가독성과 유지보수성을 크게 높여줍니다. 또한, 클린 아키텍처 패턴을 함께 사용하면 각 계층 간의 의존성을 최소화하면서 개발 과정을 더욱 쉽게 만들어주는 것 같습니다.&lt;/p&gt;</description>
      <category>안드로이드/코루틴</category>
      <category>flow</category>
      <category>안드로이드</category>
      <category>코루틴</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/47</guid>
      <comments>https://code-tales.tistory.com/47#entry47comment</comments>
      <pubDate>Thu, 21 Mar 2024 11:30:59 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 Coroutine Flow] State Flow 원자성 보장</title>
      <link>https://code-tales.tistory.com/46</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드의 State Flow는 Kotlin Coroutine의 일부로, 상태 관리를 위해 만들어진 Flow입니다. 상태가 없는 Flow에 상태를 보유할 수 있는 기능을 추가해서 개발자가 상태를 보다 쉽고 편리하게 관리할 수 있게 해 줍니다. 이번 포스팅에서는 이러한 State Flow를 사용할 때 발생할 수 있는 문제와 원자성을 보장하기 위한 해결방법에 대해 알아보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;3088&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lJA3I/btsFUDYaWmy/kjCw4ut5JLqnk31uH9ZMg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lJA3I/btsFUDYaWmy/kjCw4ut5JLqnk31uH9ZMg0/img.png&quot; data-alt=&quot;다중 스레드에서 체크 상태를 변경했을 때, 원자성이 보장되지 않은 경우 체크박스 상태가 이상해진다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lJA3I/btsFUDYaWmy/kjCw4ut5JLqnk31uH9ZMg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlJA3I%2FbtsFUDYaWmy%2FkjCw4ut5JLqnk31uH9ZMg0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;State_Flow_원자성 보장예제&quot; loading=&quot;lazy&quot; width=&quot;254&quot; height=&quot;545&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;3088&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;다중 스레드에서 체크 상태를 변경했을 때, 원자성이 보장되지 않은 경우 체크박스 상태가 이상해진다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. StateFlow 사용방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드에서 UI에 상태를 유지하고 변경할 때, StateFlow를 많이 사용합니다. 예를 들어, 아래와 같이 data class로 생성된 상태 홀더를 StateFlow로 관리할 수 있습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;data class UiState(
    val name: String = &quot;&quot;,
    val checked: Boolean = false
)

@HiltViewModel
class UiExampleViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle,
) : ViewModel() {
    private val _uiState = MutableStateFlow(UiState())
    val uiState: StateFlow&amp;lt;CheckUiState&amp;gt; = _uiState
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그리고 상태값을 변경할 때 아래와 같이 data class의 copy 함수를 통해 변경하고자 하는 프로퍼티만 편하게 변경할 수 있습니다. 상태를 관리할 때 data class를 사용하면 한 곳에서 관련된 데이터들을 확인할 수 있고, 코드 가독성이 좋아집니다. 또한, 아래 코드 예제처럼 하나 이상의 프로퍼티를 쉽게 수정할 수 있어서 UI 상태를 간단하게 업데이트할 수 있게 됩니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;_uiState.value = _uiState.value.copy(
    name = &quot;my name&quot;,
    checked = _uiState.value.checked.not(),
)

_uiState.value = _uiState.value.copy(
    name = &quot;brother's name&quot;
)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. StateFlow의 value를 통해 업데이트할 때의 문제점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;위의 예제만 보면 문제가 없는 것처럼 보이지만, 치명적인 문제가 한 가지 있습니다. 바로 다중 스레드에서 uiState의 값을 변경할 때 문제가 발생합니다.&lt;/b&gt;&lt;/span&gt; 아래 코드처럼 StateFlow의 값이 경신되기 전에 다른 스레드에서 값을 업데이트하면 원자성을 보장할 수 없게 됩니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;viewModelScope.launch(Dispatchers.IO) {
    _uiState.value = _uiState.value.copy(
        name = &quot;my name&quot;
    )
}

viewModelScope.launch(Dispatchers.Default) {
    _uiState.value = _uiState.value.copy(
        checked = _uiState.value.checked2.not(),
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;버튼을 눌렀을 때, 위의 코드가 실행되게 한다면 name은 &quot;my name&quot;이 되고 checked는 토글로 작동되는 것을 예상할 수 있습니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;그러나 실제로 10~20번 버튼을 눌러보면 원자성이 보장되지 않는 결과를 확인할 수 있습니다. 즉, 위 예제의 launch 코드가 운이 좋아서 잘 작동했던 것뿐입니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. StateFlow의 원자성을 보장하는 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다중 스레드에서 동시에 상태를 업데이트해도 원자성을 보장하기 위해서는 몇 가지 방법이 있는데, 여기서는 가장 쉬운 방법에 대해 설명하겠습니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;바로, 코틀린 코루틴 1.5.1에 추가된 update() 함수를 사용하는 것입니다. 아래는 MutableStateFlow의 update 함수 내부 모습입니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;/**
 * Updates the [MutableStateFlow.value] atomically using the specified [function] of its value.
 *
 * [function] may be evaluated multiple times, if [value] is being concurrently updated.
 */
public inline fun &amp;lt;T&amp;gt; MutableStateFlow&amp;lt;T&amp;gt;.update(function: (T) -&amp;gt; T) {
    while (true) {
        val prevValue = value
        val nextValue = function(prevValue)
        if (compareAndSet(prevValue, nextValue)) {
            return
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;이전 상태값과 현재 상태값을 비교해서 다른&lt;/span&gt; 스레드에서 수정이 됐다면 다시 루프를 수행하는 구조로 되어있습니다. compareAndSet 함수의 내부 모습을 확인해 보면 synchronized를 이용해서 원자성을 보장하고 있습니다.&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;아래 코드는 아까 작성했던 예제 코드를 update() 함수를 사용해서 리팩토링한 모습입니다. 아래와 같이 MutableStateFlow의 update() 함수를 사용하면 여러 스레드에서 동시에 상태값을 수정하는 상황에서도 안전하게 상태값을 수정할 수 있으며, 크리티컬 섹션 문제도 해결할 수 있습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;viewModelScope.launch(Dispatchers.IO) {
    _uiState.update { state -&amp;gt;
        state.copy(
            text = &quot;my name&quot;
        )
    }
}

viewModelScope.launch(Dispatchers.Default) {
    _uiState.update { state -&amp;gt;
        state.copy(
            checked = state.checked.not()
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드에서 StateFlow를 사용할 때 동시 작업으로 값을 업데이트하는 경우, 원자성이 보장되지 않는 문제와 이를 해결하기 위한 방법에 대해 알아보았습니다. MutableStateFlow의 update() 함수가 생기기 전에는 개발자가 mutex를 생성해서 관리해주어야 했지만, 코틀린 코루틴 1.5.1 이상 버전에서는 위의 예제처럼 update() 함수를 통해 편하고 안전하게 상태값을 업데이트할 수 있습니다.&lt;/p&gt;</description>
      <category>안드로이드/코루틴</category>
      <category>flow</category>
      <category>안드로이드</category>
      <category>코루틴</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/46</guid>
      <comments>https://code-tales.tistory.com/46#entry46comment</comments>
      <pubDate>Wed, 20 Mar 2024 11:30:51 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 ViewModel] Single State와 Multiple State의 차이</title>
      <link>https://code-tales.tistory.com/45</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근 안드로이드에서는 View에 State를 제공하는 형태로 UI에 데이터를 보여주고 있습니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;이때, 상태를 제공하는 방법으로는 크게 2가지로 나뉘게 됩니다. 바로, Single State와 Multiple State입니다.&lt;/b&gt;&lt;/span&gt; 각 방법마다 장단점이 존재하고 어려운 개념이 아니기에 코드 예제와 함께 설명하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;single_multiple_state.jpg&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;3088&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q1wbB/btsFU7KyTl7/7Few7KjCuwwfWv8n0I3Rs1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q1wbB/btsFU7KyTl7/7Few7KjCuwwfWv8n0I3Rs1/img.jpg&quot; data-alt=&quot;'Generate Random Data' 버튼을 누르면 카드 UI의 데이터가 변경된다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q1wbB/btsFU7KyTl7/7Few7KjCuwwfWv8n0I3Rs1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq1wbB%2FbtsFU7KyTl7%2F7Few7KjCuwwfWv8n0I3Rs1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;State 처리 방식을 확인하기 위한 UI&quot; loading=&quot;lazy&quot; width=&quot;259&quot; height=&quot;555&quot; data-filename=&quot;single_multiple_state.jpg&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;3088&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;'Generate Random Data' 버튼을 누르면 카드 UI의 데이터가 변경된다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;먼저, 아래의 3개의 변수가 필요한 상황이라고 가정하겠습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;제목&lt;/li&gt;
&lt;li&gt;내용&lt;/li&gt;
&lt;li&gt;체크박스&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Single State를 사용한 경우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Single State는 하나의 data class로 묶어서 처리하는 것을 뜻합니다. 제목, 내용, 체크박스 상태를 관리하기 위해서 아래처럼 CardUiState라는 data class를 만듭니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;data class CardUiState(
    val title: String = &quot;&quot;,
    val description: String = &quot;&quot;,
    val checked: Boolean = false
)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그리고 ViewModel에서 해당 data class를 사용하기 위해서 MutableStateOf로 선언합니다. MutableStateOf의 value 프로퍼티를 통해 데이터를 읽고 저장할 수 있습니다. 상태 변화를 확인하기 위한 generateRandomData() 함수도 만들었습니다. 해당 함수는 CardUiState의 값을 변경하는 역할을 합니다. 버튼을 눌렀을 때, 해당 함수가 실행돼서 UI의 모습이 변하는지 확인하기 위해 만들었습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@HiltViewModel
class SingleMultipleStateViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle,
) : ViewModel() {
    private val _cardState = mutableStateOf(CardUiState())
    val cardState: State&amp;lt;CardUiState&amp;gt; = _cardState

    init {
        _cardState.value = CardUiState(
            title = &quot;title&quot;,
            description = &quot;description&quot;,
            checked = true
        )
    }

    fun generateRandomData() {
        val randomTitles = listOf(&quot;Sunshine&quot;, &quot;Rainforest&quot;, &quot;Ocean Breeze&quot;, &quot;Mountain Peak&quot;, &quot;City Lights&quot;)
        val randomDescriptions = listOf(
            &quot;A bright, cheerful day with warm sunlight.&quot;,
            &quot;A dense, mysterious jungle filled with wildlife.&quot;,
            &quot;A gentle sea breeze with waves lapping at the shore.&quot;,
            &quot;A stunning view from the top of a high mountain.&quot;,
            &quot;The vibrant life and energy of a bustling city.&quot;
        )

        _cardState.value = _cardState.value.copy(
            title = randomTitles.random(),
            description = randomDescriptions.random(),
            checked = true
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이렇게 상태와 관련된 변수를 data class로 관리하기 때문에 상태를 언제든지 한 곳에서 확인할 수 있게 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-1. Single State의 장점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;화면의 상태를 한 곳에서 확인할 수 있어서 편하고 가독성이 좋습니다.&lt;/li&gt;
&lt;li&gt;상태에 대한 테스트를 작성하기 쉬워집니다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1-2. Single State의 단점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위의 코드 예시에 나온 것처럼 하나의 필드만 수정되는 경우에도 모든 필드를 복사해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Multiple State를 사용한 경우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 Multiple State 방식을 사용해서 위에서 보인 예제를 변경하겠습니다. &lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;Multiple State는 각 상태를 하나의 data class로 관리하는 것이 아니라 모든 필드가 ViewModel 내부에&lt;/span&gt; 선언돼 있는 구조입니다.&lt;/b&gt; 아래 코드는 Multiple State 방식으로 변경한 모습입니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@HiltViewModel
class SingleMultipleStateViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle,
) : ViewModel() {
    // Multiple State
    private val _title = mutableStateOf(&quot;&quot;)
    val title: State&amp;lt;String&amp;gt; = _title

    private val _description = mutableStateOf(&quot;&quot;)
    val description: State&amp;lt;String&amp;gt; = _description

    private val _checked = mutableStateOf(true)
    val checked: State&amp;lt;Boolean&amp;gt; = _checked

    init {
        _title.value = &quot;title&quot;
        _description.value = &quot;description&quot;
        _checked.value = true
    }

    fun generateRandomData() {
        val randomTitles = listOf(&quot;Sunshine&quot;, &quot;Rainforest&quot;, &quot;Ocean Breeze&quot;, &quot;Mountain Peak&quot;, &quot;City Lights&quot;)
        val randomDescriptions = listOf(
            &quot;A bright, cheerful day with warm sunlight.&quot;,
            &quot;A dense, mysterious jungle filled with wildlife.&quot;,
            &quot;A gentle sea breeze with waves lapping at the shore.&quot;,
            &quot;A stunning view from the top of a high mountain.&quot;,
            &quot;The vibrant life and energy of a bustling city.&quot;
        )
        _title.value = randomTitles.random()
        _description.value = randomDescriptions.random()
        _checked.value = true
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2-1. Multiple State의 장점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 필드가 서로 의존성을 띄지 않고 독립적으로 작동됩니다.&lt;/li&gt;
&lt;li&gt;하나의 필드만 변경될 때, 다른 필드는 변경되지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2-2. Multiple State의 단점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일관성 없게 변수를 선언하면 순서가 섞이는 등 가독성이 안 좋아질 수 있습니다.&lt;/li&gt;
&lt;li&gt;필드가 많은 경우 파악하기가 어렵습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드에서 State를 관리할 때 ViewModel 내부에서 Single State, Multiple State 2가지 방법을 사용할 수 있습니다. 사용자 입장에서도 동일한 결과를 보여주지만, 코드 관리의 측면에서는 다른 양상을 보입니다. 저는 두 가지 방법 모두 사용해 보았는데 연관된 필드가 3개 이상으로 많아질 때는 Single State 방식을 더 선호합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러분의 생각과 사례를 댓글로 공유해 주시는건 언제든지 환영합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;추가사항: 이 예제에서는 다중 스레드에 의해 State가 동시에 수정되면 발생할 수 있는 크리티컬 섹션(Critical Section) 위험성이 존재하는데 다음 포스팅에서 설명하겠습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>안드로이드/Compose State</category>
      <category>ViewModel</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/45</guid>
      <comments>https://code-tales.tistory.com/45#entry45comment</comments>
      <pubDate>Tue, 19 Mar 2024 11:30:23 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 Jetpack Compose UI] LazyRow를 사용해서 RecyclerView 구현하기</title>
      <link>https://code-tales.tistory.com/44</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 이번 시간에는 안드로이드 Jetpack Compose에서 LazyRow를 사용해 가로 방향으로 무한 스크롤이 되면서 메모리 최적화 기능이 적용된 RecyclerView를 구현하겠습니다. 기존의 안드로이드 레이아웃 XML 방식을 사용해 보신 분들이라면 RecyclerView를 만들 때, 필요한 사전작업이 많았다는 걸 아실 텐데요. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Compose로 넘어와서는 상당히 간단하게 구현할 수 있게 됐습니다. Compose에서 제공하는 LazyRow를 사용하면 가로 방향으로 무한 스크롤이 되는 RecyclerView를 만들 수 있습니다.&lt;/b&gt;&lt;/span&gt; 그럼 지금부터 코드와 함께 예제를 보여드리겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;horizontal_recycler_view.jpg&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;3088&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkRsKs/btsFI4Wk433/wGsAKmuDWddYHKITqGfMBk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkRsKs/btsFI4Wk433/wGsAKmuDWddYHKITqGfMBk/img.jpg&quot; data-alt=&quot;안드로이드 Jetpack Compose에서 구현한 Horizontal RecyclerView&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkRsKs/btsFI4Wk433/wGsAKmuDWddYHKITqGfMBk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkRsKs%2FbtsFI4Wk433%2FwGsAKmuDWddYHKITqGfMBk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;안드로이드 Jetpack Compose에서 구현한 Horizontal RecyclerView&quot; loading=&quot;lazy&quot; width=&quot;278&quot; height=&quot;596&quot; data-filename=&quot;horizontal_recycler_view.jpg&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;3088&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;안드로이드 Jetpack Compose에서 구현한 Horizontal RecyclerView&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. LazyRow를 사용해서 RecyclerView 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;LazyRow는 항목을 가로로 배치하고, 사용자가 스크롤을 통해 양옆으로 탐색할 수 있게 해 줍니다. XML 기반의 RecyclerView에서는 Adapter 클래스가 추가로 필요했지만, Compose의 LazyRow를 사용하면 가로 방향으로 무한 스크롤이 가능한 RecyclerView를 손쉽게 구현할 수 있으며, Compose의 itemsIndexed 함수를 사용해 각 항목에 고유한 인덱스를 부여할 수 있습니다.&lt;/b&gt;&lt;/span&gt; 이를 통해 정보를 제공하는 동시에 메모리 사용을 최적화할 수 있습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun CustomVerticalRecyclerView(users: List&amp;lt;ProfileEntity&amp;gt;) {
    LazyRow(
        modifier = Modifier.fillMaxSize(),
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        content = {
            itemsIndexed(users) { index, user -&amp;gt;
                CustomUserProfile(
                    id = user.id,
                    name = user.name,
                    age = user.age,
                    nickName = user.nickName,
                    hobby = user.hobby
                )
            }
        })
}

@Composable
fun CustomUserProfile(id: Int, name: String, age: Int, nickName: String, hobby: String) {
    Card(
        modifier = Modifier
            .padding(4.dp)
            .wrapContentWidth()
            .height(130.dp)
            .wrapContentWidth(Alignment.Start),
        shape = RoundedCornerShape(15.dp),
        elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
        colors = CardDefaults.cardColors(
            containerColor = Color(0xFFECEFF1),
        )
    ) {
        Row(
            modifier = Modifier
                .fillMaxHeight()
                .padding(top = 12.dp, bottom = 12.dp, start = 12.dp, end = 30.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            // 프로필 이미지나 아이콘을 여기에 넣을 수 있습니다.
            Icon(
                imageVector = Icons.Filled.AccountCircle,
                contentDescription = &quot;Profile&quot;,
                modifier = Modifier
                    .size(60.dp)
                    .padding(4.dp),
                tint = Color.Gray
            )

            Spacer(modifier = Modifier.width(12.dp))

            Column {
                Text(
                    text = &quot;# $id&quot;,
                    style = TextStyle(
                        color = Color.Black,
                        fontSize = 22.sp,
                        fontWeight = FontWeight.Bold
                    )
                )

                Spacer(modifier = Modifier.height(4.dp))

                Text(
                    text = &quot;$name a.k.a $nickName&quot;,
                    style = TextStyle(
                        color = Color.Black,
                        fontSize = 18.sp,
                        fontWeight = FontWeight.Bold
                    )
                )

                Spacer(modifier = Modifier.height(4.dp))

                Text(
                    text = &quot;Age: $age&quot;,
                    style = TextStyle(
                        color = Color.DarkGray,
                        fontSize = 14.sp
                    )
                )

                Spacer(modifier = Modifier.height(4.dp))

                Text(
                    text = &quot;Hobby: $hobby&quot;,
                    style = TextStyle(
                        color = Color.DarkGray,
                        fontSize = 14.sp
                    )
                )
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. LazyRow 사용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 모델링은 ViewModel에서 추가했습니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;서버 API 통신을 해서 데이터를 불러오는 것처럼 재현하기 위해서 generateDummyProfiles 함수를 통해 무작위 프로필 데이터를 생성하며, 이 데이터는 Composable 함수에서 LazyRow를 구성하는 데 사용했습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;data class ProfileEntity(
    val id: Int,
    val name: String,
    val age: Int,
    val nickName: String,
    val hobby: String,
)

@HiltViewModel
class HorizontalRecyclerViewViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle,
) : ViewModel() {
    val users = mutableStateOf&amp;lt;List&amp;lt;ProfileEntity&amp;gt;&amp;gt;(generateDummyProfiles())
    private fun generateDummyProfiles(): List&amp;lt;ProfileEntity&amp;gt; {
        val names = listOf(
            &quot;John&quot;, &quot;Emily&quot;, &quot;Michael&quot;, &quot;Rachel&quot;, &quot;Sophie&quot;,
            &quot;Daniel&quot;, &quot;Laura&quot;, &quot;David&quot;, &quot;Sarah&quot;, &quot;Chris&quot;,
            &quot;Anna&quot;, &quot;Max&quot;, &quot;Julia&quot;, &quot;Robert&quot;, &quot;Lisa&quot;,
            &quot;James&quot;, &quot;Maria&quot;, &quot;Paul&quot;, &quot;Alice&quot;, &quot;Tom&quot;
        )
        val nickNames = listOf(
            &quot;Skywalker&quot;, &quot;Ranger&quot;, &quot;Phoenix&quot;, &quot;Shadow&quot;, &quot;Maverick&quot;,
            &quot;Viper&quot;, &quot;Wizard&quot;, &quot;Rogue&quot;, &quot;Spartan&quot;, &quot;Hunter&quot;
        )
        val hobbies = listOf(
            &quot;Reading&quot;, &quot;Gaming&quot;, &quot;Traveling&quot;, &quot;Cooking&quot;, &quot;Photography&quot;,
            &quot;Painting&quot;, &quot;Music&quot;, &quot;Hiking&quot;, &quot;Cycling&quot;, &quot;Swimming&quot;
        )

        return List(5000) { index -&amp;gt;
            ProfileEntity(
                id = index,
                name = names.random(),
                nickName = nickNames.random(),
                hobby = hobbies.random(),
                age = (18..60).random()
            )
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;ViewModel에서 생성한 데이터를 HorizontalRecyclerViewScreen 컴포저블에서 가져와 사용합니다. UI로직과 데이터 처리를 분리한 구조이기 때문에 코드의 가독성과 관리를 향상하는데 도움을 주는 장점이 있습니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;그리고 ViewModel에서 데이터를 관리하면 안드로이드의 라이프사이클을 인식하므로 화면 회전에도 데이터 상태를 유지할 수 있습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun HorizontalRecyclerViewScreen(
    mainNavController: NavHostController,
    viewModel: HorizontalRecyclerViewViewModel
) {
    val users by remember { viewModel.users }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(top = 10.dp)
    ) {
        Spacer(modifier = Modifier.height(10.dp))

        Text(
            text = &quot;item's count : ${users.size}&quot;,
        )

        Spacer(modifier = Modifier.height(10.dp))

        CustomVerticalRecyclerView(
            users = users
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. LazyRow에서 itemIndexed 사용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LazyRow의 itemIndexed 함수를 사용하면, 각 항목에 고유 인덱스를 부여할 수 있습니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;인덱스와 각 아이템의 항목을 람다의 인자로 전달받을 수 있기 때문에 데이터 바인딩하는데 무척 편리합니다. 그래서 실무에서는 itemIndexed를 많이 사용하게 되는데, 상황에 맞춰서 item, items 함수를 사용할 때도 편리합니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: left;&quot;&gt;안드로이드 Jetpack Compose의 LazyRow를 이용한 RecyclerView 구현은 기존 XML 방식에 비해서 코드가 간결하여 간단하게 개발할 수 있습니다. 또한, 아이템 간의 간격을 추가할 때도 Arrangement.spacedBy() 함수를 통해 설정할 수 있으며 별도의 Adapter 클래스가 필요하지 않은 것도 편하게 느껴집니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: left;&quot;&gt;이 글을 통해 안드로이드 Jetpack Compose UI에서 LazyRow를 활용한 RecyclerView 구현 방법을 이해하고, 여러분의 프로젝트에 적용하는데 도움이 되기를 바랍니다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>안드로이드/Compose UI</category>
      <category>Jetpack Compose</category>
      <category>LazyRow</category>
      <category>recyclerview</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/44</guid>
      <comments>https://code-tales.tistory.com/44#entry44comment</comments>
      <pubDate>Mon, 18 Mar 2024 11:30:53 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 Jetpack Compose UI] Row를 사용해서 HorizontalScrollView 구현하기</title>
      <link>https://code-tales.tistory.com/43</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;이번 포스팅에서는 안드로이드 Jetpack Compose에서 가로방향으로 아이템이 추가되는 HorizontalScrollView를 구현하겠습니다.&lt;/b&gt;&lt;/span&gt; Compose UI에서는 가로 방향으로 UI를 배치할 때 Row 컴포저블을 사용합니다. 이때, Row에 추가 옵션을 주면 아이템이 Row 컴포저블을 벗어나도 스크롤을 통해서 UI를 확인할 수 있습니다. 이제 간단한 예제를 통해 HorizontalScrollView를 구현하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;horizontal_scroll_view.jpg&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;3088&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cIZYu2/btsFK7EhkcN/7KdHC8ohq2ULYsjHqRLBI1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cIZYu2/btsFK7EhkcN/7KdHC8ohq2ULYsjHqRLBI1/img.jpg&quot; data-alt=&quot;안드로이드 Jetpack Compose에서 구현한 HorizontalScrollView&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cIZYu2/btsFK7EhkcN/7KdHC8ohq2ULYsjHqRLBI1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIZYu2%2FbtsFK7EhkcN%2F7KdHC8ohq2ULYsjHqRLBI1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;안드로이드 Jetpack Compose에서 구현한 HorizontalScrollView&quot; loading=&quot;lazy&quot; width=&quot;231&quot; height=&quot;495&quot; data-filename=&quot;horizontal_scroll_view.jpg&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;3088&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;안드로이드 Jetpack Compose에서 구현한 HorizontalScrollView&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Row를 사용해서 HorizontalScrollView 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #0d0d0d; text-align: left;&quot;&gt;HorizontalScrollView 구현의 핵심은 Row 컴포저블과 &lt;/span&gt;horizontalScroll&lt;span style=&quot;color: #0d0d0d; text-align: left;&quot;&gt; modifier의 조합입니다. Row는 자식 컴포저블들을 가로 방향으로 나열하며, &lt;/span&gt;horizontalScroll&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;을 사용하면 가로로 스크롤 가능한 레이아웃을 만들 수 있습니다. 이렇게 하면 사용자가 스크롤을 통해 화면 너비를 초과하는 아이템들을 탐색할 수 있게 됩니다.&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: left;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: left;&quot;&gt;또한, Compose의 Arrangement.spacedBy를 사용해서 아이템 사이의 간격을 설정했습니다. Spacer를 추가해도 되지만 Arragement.spacedBy를 통해 가능하다는 점도 보여드리고 싶었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1710237907039&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun CustomHorizontalScrollView(items: List&amp;lt;String&amp;gt;, scrollState: ScrollState) {
    Row(
        modifier = Modifier
            .fillMaxSize()
            .horizontalScroll(scrollState),
		horizontalArrangement = Arrangement.spacedBy(8.dp) // 여기에 간격 지정
    ) {
        for (item in items) {
            CustomCardView(item)
        }
    }
}

@Composable
fun CustomCardView(text: String) {
    Card(
        modifier = Modifier
            .width(150.dp)
            .height(60.dp),
        shape = RoundedCornerShape(10.dp),
        border = BorderStroke(1.dp, Color.LightGray),
        colors = CardDefaults.cardColors(
            containerColor = Color(0xFFFF5F5F),
        )
    ) {
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .fillMaxHeight()
                .padding(start = 10.dp)
                .background(Color.White),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.Start
        ) {
            Text(
                modifier = Modifier.padding(start = 10.dp),
                textAlign = TextAlign.Center,
                text = &quot;CardView $text&quot;
            )
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Row를 HorizontalScrollView처럼 사용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;HorizontalScrollView 구현의 다음 단계는 스크롤 상태 관리입니다. rememberScrollState 함수를 사용해 Row의 스크롤 상태를 추적하고 관리합니다. 이를 통해 사용자가 가로로 스크롤을 할 때 Row 내부의 아이템들이 원활하게 이동하도록 합니다.&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CustomHorizontalScrollView는 필요에 따라 다양한 아이템을 동적으로 표시할 수 있습니다. 해당 예제에서 각 아이템은 CustomCardView를 통해 구현했고, 이런 방식으로 Compose를 사용하면 복잡한 리스트나 스크롤 뷰를 기존 안드로이드 레이아웃 XML 구현보다 간단하고 효율적으로 구현할 수 있습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun HorizontalScrollViewScreen(
    mainNavController: NavHostController,
    viewModel: HorizontalScrollViewViewModel
) {
    val scrollState = rememberScrollState()
    val items = List(100) { it.toString() }

    Column(
        modifier = Modifier
            .fillMaxSize()
    ) {
        Spacer(modifier = Modifier.height(10.dp))

        Text(
            text = &quot;item's count : ${items.size}&quot;,
        )

        Spacer(modifier = Modifier.height(10.dp))

        CustomHorizontalScrollView(
            items = items,
            scrollState = scrollState
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Row를 사용한 HorizontalScrollView의 한계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Row를 이용한 HorizontalScrollView 구현 방법은 매우 편리하고 간단하지만, 대량의 아이템을 다룰 경우 메모리 관리와 퍼포먼스 측면에서 한계를 갖게 됩니다. 각 아이템이 Row 내부에서 개별적으로 관리되기 때문에 아이템 수가 많을수록 메모리 사용량이 늘어납니다. 그래서 아이템이 많으면 많을수록 해당 페이지에 진입하는데 시간이 오래 걸리기도 하고 안드로이드 ANR 오류가 발생할 수 있습니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;이런 경우, 안드로이드 Compose에서 RecyclerView에 해당되는 LazyRow를 사용해야 합니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 Jetpack Compose를 사용하여 HorizontalScrollView를 구현하는 것은 Row 컴포저블과 함께 ScrollState를 사용하면, 손쉽게 가로 스크롤 가능한 뷰를 만들 수 있으며, Compose의 선언적 UI 패러다임을 통해 개발 과정을 단순화하고 효율적으로 만들 수 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;하지만 아이템의 개수가 많아지는 경우에는 모든 아이템이 메모리에 올라가기 때문에 아이템이 많은 경우에는 Row 기반의 HorizontalScrollView를 사용하면 안 됩니다. 이런 경우에는 RecyclerView처럼 작동하는 LazyRow를 사용해야 합니다.&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글을 통해 안드로이드 Jetpack Compose UI에서 HorizontalScrollView를 만드는데 도움이 되기를 바랍니다. 다음 포스팅에서는 Row기반의 ScrollView 한계를 극복할 수 있는 LazyRow의 구현 방법에 대해 알아보겠습니다.&lt;/p&gt;</description>
      <category>안드로이드/Compose UI</category>
      <category>Jetpack Compose</category>
      <category>Row</category>
      <category>ScrollView</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/43</guid>
      <comments>https://code-tales.tistory.com/43#entry43comment</comments>
      <pubDate>Sun, 17 Mar 2024 11:30:53 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 Jetpack Compose] remember와 MutableState의 3가지 사용 방법</title>
      <link>https://code-tales.tistory.com/42</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 이번 포스팅에서는 안드로이드 Jetpack Compose에서 상태 변수를 관리하기 위해서 필수적으로 사용하는 remember와 MutableState에 대해서 설명하고, MutableState의 3가지 사용방법을 코드 예제와 함께 살펴보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ui_update_loop.png&quot; data-origin-width=&quot;856&quot; data-origin-height=&quot;430&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ck4HuY/btsFOh8camI/V8IEjk7QphCTGzMolMxqlk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ck4HuY/btsFOh8camI/V8IEjk7QphCTGzMolMxqlk/img.png&quot; data-alt=&quot;상태값 변경에 따른 업데이트 루프&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ck4HuY/btsFOh8camI/V8IEjk7QphCTGzMolMxqlk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fck4HuY%2FbtsFOh8camI%2FV8IEjk7QphCTGzMolMxqlk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;상태값 변경에 따른 업데이트 루프&quot; loading=&quot;lazy&quot; width=&quot;492&quot; height=&quot;247&quot; data-filename=&quot;ui_update_loop.png&quot; data-origin-width=&quot;856&quot; data-origin-height=&quot;430&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;상태값 변경에 따른 업데이트 루프&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. MutableState와 Remember 개념&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 Jetpack Compose에서 UI 로직을 처리할 때는 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;주로 mutableStateOf와 remember를 사용합니다. 여기서 compose가 생소하신 분들은 remember의 역할에 대해 모를 수 있습니다.&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Compose UI는 선언형 UI 패러다임을 따르는 방식으로써 상태가 변할 때마다 렌더링을 다시 합니다. 이렇게 다시 렌더링이 되는 현상을 Compose에서는 리컴포지션이라고 부릅니다. 리컴포지션이 발생하면 컴포넌트 안에 선언한 변수도 다시 초기화가 진행되는데, 최초에만 초기화를 진행하고 이전 상태값을 기억하기 위해서는 remember를 사용해야 합니다.&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;아래 코드에서 remember를 사용하지 않았다면 사용자가 TextField의 값을 바꿨을 때, 다시 &quot;initial name&quot;으로 초기화돼서 값이 변경되지 않는 현상이 발생합니다. 그래서 mutableStateOf와 함께 remember를 사용하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun Memo() {
    val memo = remember { mutableStateOf(&quot;initial name&quot;) }
    TextField(
        value = memo.value,
        onValueChange = { memo.value = it }
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. MutableState의 사용방법 3가지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 선언한 mutableStateOf는 3가지 방법으로 사용할 수 있습니다. 어떤 방법으로 사용해도 상관은 없지만, 다른 사람이 작업한 내용을 확인할 때 모든 사용방법을 알고 있어야 코드를 쉽게 파악할 수 있으므로 모두 알아두는 것이 좋습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1. mutableStateOf 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 예제에서 사용했던 방법입니다. 개인적으로는 해당 방법이 가장 기본적인 사용방법이라고 생각합니다. 그러나 value 프로퍼티에 값이 저장돼 있으므로 memo.value와 같이 코드가 좀 더 길어지는 단점이 있습니다. 그래서 저는 by 키워드를 선호하는 편입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2. by mutableStateOf 방법 (Delegation)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;by 키워드를 사용하면 getter와 setter를 해당 상태값에 위임하게 됩니다. 아까는 value 프로퍼티에 접근을 해서 값을 가져오거나 수정해 주었지만 by 키워드를 사용하면 상태값에 직접 접근할 수 있습니다.&lt;/b&gt;&lt;/span&gt; 즉, 위의 예제는 아래의 코드처럼 수정할 수 있습니다. 여기서 setter도 위임받았으므로 가변 변수인 var로 변경해야 합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun Memo() {
    var memo by remember { mutableStateOf(&quot;initial name&quot;) }
    TextField(
        value = memo,
        onValueChange = { memo = it }
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-3. 코틀린 구조분해할당 방법 (Destructuring)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;구조분해란 객체가 관리하는 값을 한 번에 받을 수 있는 기능입니다. 타입스크립트로 웹 개발을 해본 분들이라면 구조분해할당 방법에 친숙하실 수 있습니다.&lt;/b&gt;&lt;/span&gt; 코틀린도 이러한 구조분해할당 문법을 지원하고 있으며, MutableState에 해당 기능을 포함합니다. 아래는 구조분해할당 방법을 제공하기 위한 MutableState의 내부 코드입니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Stable
interface MutableState&amp;lt;T&amp;gt; : State&amp;lt;T&amp;gt; {
    override var value: T
    operator fun component1(): T
    operator fun component2(): (T) -&amp;gt; Unit
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그래서 MutableState를 사용할 때, 아래처럼 구조분해할당 방법으로 사용할 수 있습니다. component1에 해당되는 상태값과 component2에 해당되는 상태값 저장 함수를 동시에 할당받아서 사용한 있는 모습입니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun Memo() {
    val (memo, setMemo) = remember { mutableStateOf(&quot;initial name&quot;) }
    TextField(
        value = memo,
        onValueChange = { setMemo(it) }
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. MutableState, Remember 사용방법 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 시간에는 안드로이드 Jetpack Compose에서 MutableState와 Remember의 역할과 사용방법에 대해 알아보았습니다. MutableState를 사용할 때는 3가지 방법이 제공되고 있으며, 각 방법 모두 동일한 기능을 수행합니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;다만, getter와 setter를 위임하는 by 키워드 방법과 구조분해할당 방법을 적절하게 사용하면 중복 코드를 많이 줄일 수 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로는 by 키워드 방법을 많이 사용합니다. 변수 하나로 간단하게 getter와 setter 기능을 모두 수행하는 양방향 바인딩 스타일의 코딩 방식을 선호하기 때문입니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 어떤 방법을 사용해도 문제는 없으며 여러분의 코딩 스타일이나 회사의 코딩 컨벤션에 알맞게 사용하면 좋을 것 같습니다.&lt;/p&gt;</description>
      <category>안드로이드/Compose State</category>
      <category>Jetpack Compose</category>
      <category>MutableState</category>
      <category>State</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/42</guid>
      <comments>https://code-tales.tistory.com/42#entry42comment</comments>
      <pubDate>Sat, 16 Mar 2024 11:30:24 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 Jetpack Compose UI] LazyColumn을 사용해서 무한 스크롤 뷰 만들기</title>
      <link>https://code-tales.tistory.com/41</link>
      <description>&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;안녕하세요. 이번 포스팅에서는 안드로이드 Jetpack Compose에서 무한스크롤 뷰를 만드는 방법에 대해 설명하겠습니다. 안드로이드 Compose UI에서는 기존 XML 기반의 레이아웃에서 사용했던 Adapter 없이 간단하게 무한스크롤 뷰를 구성할 수 있습니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;바로 LazyColumn 컴포저블을 사용하면 됩니다. 선언적 UI 구성의 장점을 살려서 간결하고 유연한 코드 작성이 가능합니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;recycler_view.jpg&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;3088&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9CFNB/btsFJBZB3Op/pmiPzLbNkN6imr3Wx4gkgK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9CFNB/btsFJBZB3Op/pmiPzLbNkN6imr3Wx4gkgK/img.jpg&quot; data-alt=&quot;안드로이드 Jetpack Compose에서 구현한 Vertical 무한 스크롤 뷰&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9CFNB/btsFJBZB3Op/pmiPzLbNkN6imr3Wx4gkgK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9CFNB%2FbtsFJBZB3Op%2FpmiPzLbNkN6imr3Wx4gkgK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;안드로이드 Jetpack Compose에서 구현한 Vertical RecyclerView&quot; loading=&quot;lazy&quot; width=&quot;255&quot; height=&quot;547&quot; data-filename=&quot;recycler_view.jpg&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;3088&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;안드로이드 Jetpack Compose에서 구현한 Vertical 무한 스크롤 뷰&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;1. LazyColumn을 사용해서 무한 스크롤 뷰 만들기&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;LazyColumn은 Compose에서 제공하는 컴포저블로 기존의 RecyclerView에서의 기능을 수행합니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;XML 레이아웃 기반 RecyclerView의 어댑터 패턴과는 다르게 LazyColumn에서는 Compose의 선언적 방식을 통해 각 아이템을 간단하게 렌더링 할 수 있습니다. LazyColumn은 아이템을 동적으로 로드하며, 뷰 홀더 패턴을 내부에서 처리하기 때문에 불필요한 렌더링을 최소화하고 성능을 최적화합니다.&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아래의 코드는 사용자 데이터를 나타내는 CustomUserProfile 컴포저블을 LazyColumn에 넣어서 여러 사용자 정보를 리스트 형식으로 표시하는 예제입니다. 이때, &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;LazyColumn의 itemsIndexed 함수를 사용해서 데이터 리스트와 인덱스를 활용해 각 사용자의 정보를 표시했습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun CustomRecyclerView(users: List&amp;lt;UserEntity&amp;gt;) {
    LazyColumn(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.spacedBy(8.dp),
        content = {
            itemsIndexed(users) { index, user -&amp;gt;
                CustomUserProfile(
                    id = user.id,
                    name = user.name,
                    age = user.age
                )
            }
        })
}


@Composable
fun CustomUserProfile(id: Int, name: String, age: Int) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .height(100.dp),
        shape = RoundedCornerShape(10.dp),
        border = BorderStroke(1.dp, Color.LightGray),
        colors = CardDefaults.cardColors(
            containerColor = Color(0xFFFFC20D),
        )
    ) {
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .fillMaxHeight()
                .padding(start = 10.dp)
                .background(Color.White),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.Start
        ) {

            Text(
                modifier = Modifier.padding(start = 10.dp),
                style = TextStyle(
                    color = Color.Black,
                    fontSize = 20.sp,
                    fontWeight = FontWeight.Bold
                ),
                text = &quot;# $id&quot;
            )

            Spacer(modifier = Modifier.height(10.dp))

            Text(
                modifier = Modifier.padding(start = 10.dp),
                style = TextStyle(
                    color = Color.Black,
                    fontSize = 16.sp,
                    fontWeight = FontWeight.SemiBold
                ),
                text = &quot;Name : $name&quot;
            )

            Spacer(modifier = Modifier.height(5.dp))

            Text(
                modifier = Modifier.padding(start = 10.dp),
                style = TextStyle(
                    color = Color.Gray,
                    fontSize = 14.sp,
                    fontWeight = FontWeight.Normal
                ),
                text = &quot;Age : $age&quot;
            )
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;2. LazyColumn 사용하기&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ViewModel내에 LazyColumn에서 사용될 데이터를 추가했습니다. ViewModel에서 사용자 데이터 리스트를 생성하고, 이를 LazyColumn에 전달했습니다. 이를 통해서 사용자 인터랙션에 따라 데이터를 업데이트하거나 변경할 수 있습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;data class UserEntity(
    val id: Int,
    val name: String,
    val age: Int,
)

@HiltViewModel
class RecyclerViewViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle,
) : ViewModel() {
    val users = mutableStateOf&amp;lt;List&amp;lt;UserEntity&amp;gt;&amp;gt;(generateDummyUsers())

    private fun generateDummyUsers(): List&amp;lt;UserEntity&amp;gt; {
        val names = listOf(
            &quot;Olivia&quot;, &quot;Emma&quot;, &quot;Ava&quot;, &quot;Charlotte&quot;, &quot;Sophia&quot;,
            &quot;Amelia&quot;, &quot;Isabella&quot;, &quot;Mia&quot;, &quot;Evelyn&quot;, &quot;Harper&quot;,
            &quot;Liam&quot;, &quot;Noah&quot;, &quot;Oliver&quot;, &quot;Elijah&quot;, &quot;William&quot;,
            &quot;James&quot;, &quot;Benjamin&quot;, &quot;Lucas&quot;, &quot;Henry&quot;, &quot;Alexander&quot;
        )

        return List(5000) { index -&amp;gt;
            UserEntity(
                id = index,
                name = names.random(),
                age = (18..60).random()
            )
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;RecyclerViewScreen에서는 ViewModel에서 생성한 users 리스트를 가져와서 CustomRecyclerView의 인자로 전달하고 있습니다. by 키워드를 사용해서 users가 바로 데이터를 가리키도록 구현했습니다. 만약에 by를 사용하지 않는다면 users.value로 접근할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1710157543122&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun RecyclerViewScreen(
    mainNavController: NavHostController,
    viewModel: RecyclerViewViewModel
) {
    val users by remember { viewModel.users }
    
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(start = 10.dp, end = 10.dp)
    ) {
        Spacer(modifier = Modifier.height(10.dp))

        Text(
            text = &quot;item's count : ${users.size}&quot;,
        )

        Spacer(modifier = Modifier.height(10.dp))

        CustomRecyclerView(
            users = users
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;3. LazyColumn에서 itemIndexed 사용하기&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;LazyColumn에서는 item, items, itemIndexed 함수를 제공하는데, &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;itemIndexed 함수를 사용하면 각 아이템의 인덱스와 데이터를 함께 사용할 수 있습니다. 실무에서는 대부분 itemIndexed를 많이 사용하게 됩니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;4. 정리&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;안드로이드 Jetpack Compose의 LazyColumn을 이용한 무한 스크롤 뷰 구현은 기존 XML 레이아웃 방식에 비해서 간결하고 효율적입니다. 선언적 UI 방식을 사용함으로써 직관적으로 UI를 설계할 수 있고, 별도의 Adapter가 필요하지 않기 때문에 코드와 파일도 많이 줄어듭니다. 이번 글을 통해 여러분만의 RecyclerView를 구현하는데 도움이 됐으면 좋겠습니다.&lt;/p&gt;</description>
      <category>안드로이드/Compose UI</category>
      <category>Jetpack Compose</category>
      <category>LazyColumn</category>
      <category>recyclerview</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/41</guid>
      <comments>https://code-tales.tistory.com/41#entry41comment</comments>
      <pubDate>Fri, 15 Mar 2024 11:30:45 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 Jetpack Compose UI] Column을 사용해서 VerticalScrollView 구현하기</title>
      <link>https://code-tales.tistory.com/40</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 이번 포스팅에서는 안드로이드 Jetpack Compose에서 VerticalScrollView 만드는 방법에 대해 설명하겠습니다. Compose UI에서는 Column 컴포저블을 통해 세로로 UI를 배치할 수 있고, &lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;Column 컴포저블의 Modifier에 ScrollState를 전달해 줌으로써 Scrollable 하게 만들 수 있습니다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;3088&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chcFDk/btsFJnAvSm0/xQ1kz0FZgWh7KN6sdAiXf1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chcFDk/btsFJnAvSm0/xQ1kz0FZgWh7KN6sdAiXf1/img.jpg&quot; data-alt=&quot;안드로이드 Jetpack Compose에서 구현한 VerticalScrollView&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chcFDk/btsFJnAvSm0/xQ1kz0FZgWh7KN6sdAiXf1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchcFDk%2FbtsFJnAvSm0%2FxQ1kz0FZgWh7KN6sdAiXf1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;안드로이드 Jetpack Compose에서 구현한 VerticalScrollView&quot; loading=&quot;lazy&quot; width=&quot;256&quot; height=&quot;549&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;3088&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;안드로이드 Jetpack Compose에서 구현한 VerticalScrollView&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Column을 사용해서 VerticalScrollView 만들기&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 Compose에서는 기존의 안드로이드 개발에서 보던 ScrollView와는 다른 접근 방식을 제공합니다. &lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;가장 간단하게 세로 스크롤 뷰를 만드는 방법은 Column 컴포저블에 verticalScroll Modifier를 적용하는 것입니다. 해당 Scroll State를 통해 간단하게 스크롤뷰를 만들 수 있습니다.&lt;/span&gt;&lt;/b&gt;&lt;br&gt;&lt;br&gt;아래 예제에서는 CustomCardView 컴포저블을 만들어서 간단한 리스트 아이템을 구성하고, Column 내부에 반복문을 실행해서 5000개의 아이템을 만들었습니다. 세로 스크롤을 통해 5000번째까지의 CustomCardView를 확인할 수 있습니다.&lt;/p&gt;&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt; 
 &lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun CustomScrollView(items: List&amp;lt;String&amp;gt;, scrollState: ScrollState) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .verticalScroll(scrollState),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        for (item in items) {
            CustomCardView(item)
        }
    }
}

@Composable
fun CustomCardView(text: String) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .height(60.dp),
        shape = RoundedCornerShape(10.dp),
        border = BorderStroke(1.dp, Color.LightGray),
        colors = CardDefaults.cardColors(
            containerColor = Color(0xFFFF5F5F),
        )
    ) {
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .fillMaxHeight()
                .padding(start = 10.dp)
                .background(Color.White),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.Start
        ) {
            Text(
                modifier = Modifier.padding(start = 10.dp),
                textAlign = TextAlign.Center,
                text = &quot;CardView $text&quot;
            )
        }
    }
}&lt;/code&gt;&lt;/pre&gt; 
&lt;/div&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Column을 VerticalScrollView처럼 사용하기&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;호출자 컴포저블에서 rememberScrollState를 통해 ScrollState를 생성하고, CustomScrollView 컴포저블에 전달했습니다. 이렇게 함으로써 상태를 공유하고 스크롤 동작을 연동할 수 있습니다. 현재 예제에서는 수직 스크롤을 구현하고 있는데 수평 방향으로 스크롤이 되는 리스트뷰가 필요하다면 Column대신 Row를 사용하면 됩니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun ScrollViewScreen(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;mainNavController: NavHostController,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;viewModel: ScrollViewViewModel
) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;val scrollState = rememberScrollState()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;val items = List(5000) { it.toString() }

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Column(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;modifier = Modifier
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.fillMaxSize()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.padding(start = 10.dp, end = 10.dp)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Spacer(modifier = Modifier.height(10.dp))

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Text(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;text = &quot;item's count : ${items.size}&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;Spacer(modifier = Modifier.height(10.dp))

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;CustomScrollView(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;items = items,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;scrollState = scrollState
&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;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Column을 사용한 VerticalScrollView의 한계&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Column과 ScrollState를 사용하는 방법은 단순하고 직관적이지만 성능에 있어서 한계가 있습니다. &lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;현재 예제에서는 5000개의 아이템을 렌더링 하고 있어서 해당 화면으로 진입할 때 약 3~4초의 딜레이가 발생하고 있습니다. 그래서 많은 수의 아이템을 렌더링 하는 경우에는 ANR(Application Not Responding) 오류가 발생할 수 있습니다. 이런 경우에는 LazyColumn을 사용하는 것이 좋습니다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 정리&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 Jetpack Compose UI에서는 Column 컴포저블과 ScrollState를 사용해서 매우 간단하게 ScrollView를 구현할 수 있고, Column 컴포저블이 아닌 Row 컴포저블을 사용한다면 Horizontal ScrollView를 구현할 수 있습니다. &lt;b&gt;&lt;span style=&quot;background-color: #F6E199;&quot;&gt;하지만 복잡한 레이아웃이나 아이템 수가 많은 경우에 Column 컴포저블과 ScrollState를 사용하면 모든 UI가 메모리에 올라가므로 성능에 좋지 않고 화면이 멈추는 등의 문제가 발생합니다. 그래서 Column과 ScrollState의 조합은 간단한 레이아웃을 구성하는데 스크롤이 필요한 경우에만 사용하는 것을 권장하고 있습니다.&lt;/span&gt;&lt;/b&gt;&lt;br&gt;&lt;br&gt;다음 포스팅에서는 이러한 한계를 극복할 수 있는 LazyColumn을 사용한 안드로이드 Jetpack Compose에서의 RecyclerView 대해 포스팅하겠습니다.&lt;/p&gt;</description>
      <category>안드로이드/Compose UI</category>
      <category>Column</category>
      <category>Jetpack Compose</category>
      <category>ScrollView</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/40</guid>
      <comments>https://code-tales.tistory.com/40#entry40comment</comments>
      <pubDate>Thu, 14 Mar 2024 11:30:44 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 Jetpack Compose UI] TabPager 구현하기</title>
      <link>https://code-tales.tistory.com/39</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 이번 포스팅에서는 안드로이드 Jetpack Compose에서 TabPager를 구현하겠습니다. TabPager는 상당히 많이 사용되는 UI 중 하나입니다. TabPager를 구현할 때는 ViewPager를 연동해서 사용해야 하는데, 기존 xml 방식으로 UI를 개발할 때는 Adapter 클래스를 생성해야만 했고 연동 과정이 복잡했습니다. 그런데 안드로이드 Compose에서는 이러한 복잡한 과정이 사라졌으며 비교적 간단하게 TabPager를 구현할 수 있습니다. 이제 안드로이드 Jetpack Compose에서 TabPager를 구현하는 방법에 대해 설명하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;tab_pager.jpg&quot; data-origin-width=&quot;1162&quot; data-origin-height=&quot;2483&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpabxY/btsFFPdM2S8/DBcrQTGXFKzqyionw1qNW0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpabxY/btsFFPdM2S8/DBcrQTGXFKzqyionw1qNW0/img.jpg&quot; data-alt=&quot;안드로이드 Jetpack Compose에서 구현한 TabPager&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpabxY/btsFFPdM2S8/DBcrQTGXFKzqyionw1qNW0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpabxY%2FbtsFFPdM2S8%2FDBcrQTGXFKzqyionw1qNW0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;안드로이드 Jetpack Compose에서 구현한 TabPager&quot; loading=&quot;lazy&quot; width=&quot;230&quot; height=&quot;491&quot; data-filename=&quot;tab_pager.jpg&quot; data-origin-width=&quot;1162&quot; data-origin-height=&quot;2483&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;안드로이드 Jetpack Compose에서 구현한 TabPager&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. TabPager 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;TabPager를 구현하기 위해 먼저 TabRow와 HorizontalPager를 연동하는 작업이 필요합니다.&lt;/b&gt;&lt;/span&gt; ScrollableTabRow는 탭을 표시하고, HorizontalPager는 각 탭에 해당하는 페이지를 보여줍니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;먼저, ScrollableTabRow 내부에 Tab 컴포저블을 사용하여 탭들을 만들고, 사용자가 탭을 선택할 때마다 HorizontalPager의 페이지가 변경되도록 설정합니다. HorizontalPager의 상태는 rememberPagerState()를 사용하여 관리합니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1710065764826&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun CustomTabPager(pagerState: PagerState, tabs: List&amp;lt;String&amp;gt;) {

    val coroutineScope = rememberCoroutineScope() // 코루틴 스코프 생성

    Column {
        // 탭 구현
        ScrollableTabRow(
            selectedTabIndex = pagerState.currentPage,
            indicator = { tabPositions -&amp;gt;
                TabRowDefaults.SecondaryIndicator(
                    modifier = Modifier.tabIndicatorOffset(tabPositions[pagerState.currentPage]),
                    color = Color.Red, // 인디케이터 색상 변경
                )
            },
            divider = {}, // 빈 컴포저블을 지정하여 경계선 제거
            containerColor = Color.White, // 배경색 설정
            contentColor = Color.Red,
            edgePadding = 0.dp, // 패딩 설정
        ) {
            tabs.forEachIndexed { index, title -&amp;gt;
                Tab(
                    text = { Text(title) },
                    selected = pagerState.currentPage == index,
                    modifier = Modifier
                        .width(100.dp)  // 탭의 가로 크기
                        .height(50.dp), // 탭의 세로 크기
                    onClick = {
                        coroutineScope.launch {
                            pagerState.animateScrollToPage(index)
                        }
                    }
                )
            }
        }
        HorizontalDivider(
            modifier = Modifier
                .fillMaxWidth(), // 화면 전체 너비
            color = Color.LightGray, // 테두리 색상
            thickness = 1.dp // 테두리 두께
        )

        // 뷰페이저 구현
        HorizontalPager(
            state = pagerState
        ) { page -&amp;gt;
            Column(
                modifier = Modifier
                    .fillMaxSize()
                    .background(Color.White),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                // 페이지별 컨텐츠
                when (page) {
                    0 -&amp;gt; HomeScreen()
                    1 -&amp;gt; SettingsScreen()
                    2 -&amp;gt; ProfileScreen()
                }
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;TabPager를 사용할 때는 탭과 페이지 간의 상태를 동기화해야 합니다. 이를 위해 HorizontalPager의 currentPage 상태를 ScrollableTabRow의 selectedTabIndex에 연결합니다.&lt;/b&gt;&lt;/span&gt; 사용자가 탭을 클릭하면 HorizontalPager의 현재 페이지가 변경되도록 설정하고, 페이지를 슬라이드 할 때 해당하는 탭이 강조되도록 연동합니다.&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;2. TabPager 사용하기&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위에서 만든 TabPager를 사용하는 방법은 간단합니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;TabPager를 사용하고자 하는 컴포저블 함수에서 CustomTabPager를 생성하고, 매개변수로 PagerState와 tab을 구성하는 tabs 배열을 전달하면 됩니다.&lt;/b&gt;&lt;/span&gt; PagerState는 rememberPagerState를 통해 생성할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1710066026206&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun TabPagerScreen(
    mainNavController: NavHostController,
    viewModel: TabPagerViewModel
) {
    val tabs = listOf(&quot;홈&quot;, &quot;설정&quot;, &quot;프로필&quot;)
    val pagerState = rememberPagerState {
        tabs.size
    }

    Column {
        CustomTabPager(
            pagerState = pagerState,
            tabs = tabs
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;3. 안드로이드 Jetpack Compose TabPager 정리&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;안드로이드 Jetpack Compose를 이용한 TabPager 구현은 기존의 복잡한 구현 방식에 비해 훨씬 간결하고 직관적입니다. TabRow와 HorizontalPager를 적절히 연동하면, 사용자에게 매끄러운 탐색 경험을 제공하는 TabPager를 쉽게 만들 수 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 포스팅을 통해 Jetpack Compose를 이용한 TabPager 구현 방법을 간략히 소개했습니다. 해당 예제에서는 탭의 크기, 인디케이터 색상, 하단 구분선 등 UI를 커스텀할 수 있는 요소도 구현했습니다. 안드로이드 Jetpack Compose를 활용한 다양한 UI 구현 방법을 알고 싶은 분들에게 이 포스팅이 도움이 되길 바랍니다.&lt;/p&gt;</description>
      <category>안드로이드/Compose UI</category>
      <category>Jetpack Compose</category>
      <category>TabPager</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/39</guid>
      <comments>https://code-tales.tistory.com/39#entry39comment</comments>
      <pubDate>Wed, 13 Mar 2024 11:30:24 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 Jetpack Compose UI 시리즈] ViewPager 구현하기</title>
      <link>https://code-tales.tistory.com/38</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 이번 포스팅에서는 안드로이드 Jetpack Compose에서 ViewPager를 구현하겠습니다. ViewPager는 최근 앱에서 많이 사용되고 있는 대표적인 UI 중 하나입니다. 기존 xml에서는 ViewPager를 구현하기 위해 별도의 Adapter를 생성하고, item에 해당되는 xml도 생성해줘야 했습니다. 그런데 안드로이드 Jetpack Compose에서는 상당히 편하게 구현할 수 있습니다. 이제 안드로이드 Jetpack Compose에서 ViewPager를 구현하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;view_pager.jpg&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;1544&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dw1Znd/btsFEZzb0G7/eRlfjUO8LuKWVN7ofQqvt0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dw1Znd/btsFEZzb0G7/eRlfjUO8LuKWVN7ofQqvt0/img.jpg&quot; data-alt=&quot;안드로이드 Jetpack Compose에서 구현한 ViewPager&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dw1Znd/btsFEZzb0G7/eRlfjUO8LuKWVN7ofQqvt0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdw1Znd%2FbtsFEZzb0G7%2FeRlfjUO8LuKWVN7ofQqvt0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;안드로이드 Jetpack Compose에서 구현한 ViewPager&quot; loading=&quot;lazy&quot; width=&quot;242&quot; height=&quot;519&quot; data-filename=&quot;view_pager.jpg&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;1544&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;안드로이드 Jetpack Compose에서 구현한 ViewPager&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. ViewPager 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;좌우로 넘어가는 ViewPager를 만들기 위해서 androidx.compose.foundation.pager의 HorizontalPager를 사용했습니다. 만약에 위아래로 넘길 수 있는 ViewPager를 만들려면 VerticalPager를 사용하면 됩니다.&lt;/b&gt;&lt;/span&gt; HorizontalPager는 state만 제공하면 사용할 수 있는데, &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;rememberPagerState를 사용해서 페이저 상태 홀더 객체를 얻을 수 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rememberPagerState 함수를 사용하여 생성된 PagerState는 현재 페이지의 인덱스, 스와이프 상태 등을 관리합니다. 이 상태는 CustomViewPager의 호출자에 의해 생성되고 관리되게 구현했습니다. 각 페이지는 Card 컴포저블로 렌더링 되며, Column을 사용해서 카드 내부의 Text 컴포저블을 가운데에 정렬되게 했습니다. 아래는 예시 코드입니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun CustomViewPager(pagerState: PagerState) {
    HorizontalPager(
        modifier = Modifier.fillMaxSize(),
        verticalAlignment = Alignment.CenterVertically,

        state = pagerState
    ) {
        Card(
            modifier = Modifier
                .fillMaxWidth()
                .height(100.dp)
                .padding(horizontal = 20.dp),
            shape = RoundedCornerShape(8.dp),
            border = BorderStroke(1.dp, Color.Black),
            colors = CardDefaults.cardColors(
                containerColor = Color.White,
            ),
            elevation = CardDefaults.cardElevation(10.dp),
        ) {
            Column(
                modifier = Modifier.fillMaxSize(),
                verticalArrangement = Arrangement.Center, // 세로 방향으로 가운데 정렬
                horizontalAlignment = Alignment.CenterHorizontally // 가로 방향으로 가운데 정렬
            ) {
                Text(
                    text = &quot;Page $it&quot;,
                )
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;CustomViewPager 컴포저블 함수를 만들었습니다. 재사용성을 높이기 위해서 PagerState를 CustomViewPager 호출자로부터 전달받도록 상태 호이스팅 패턴을 이용했습니다. &lt;/b&gt;&lt;/span&gt;상태 호이스팅 패턴을 이용한 이유는 CustomViewPager 컴포저블이 더 유연하고 재사용이 가능해지기 때문입니다. &lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;상태 호이스팅이란, 컴포저블 함수 자체가 상태를 가지지 않고 부모 컴포저블로부터 전달받아 사용하는 패턴입니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. ViewPager 사용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 만든 ViewPager를 사용하는 방법은&amp;nbsp;엄청 간단합니다. 이번 예제에서는 ViewPagerScreen 컴포저블에서 CustomViewPager 컴포저블을 호출해서 ViewPager를 사용합니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;이때, rememberPagerState로 생성한 pagerState를 CustomViewPager에 전달해서 페이지 상태를 관리합니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun ViewPagerScreen(
    mainNavController: NavHostController,
    viewModel: ViewPagerViewModel
) {

    val pagerState = rememberPagerState {
        10 // 총 페이지 수 설정
    }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(10.dp)
    ) {
        Text(
            text = &quot;Hello Android Compose ViewPager&quot;
        )

        Spacer(modifier = Modifier.height(20.dp))

		// 현재 페이지 수와 현재 페이지 인덱스를 보여줌
        Text(
            text = &quot;pageCount : ${pagerState.pageCount}&quot;
        )
        Text(
            text = &quot;currentPage : ${pagerState.currentPage}&quot;
        )

        Spacer(modifier = Modifier.height(20.dp))

		// CustomViewPager에 pagerState를 전달
        CustomViewPager(pagerState = pagerState)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 안드로이드 Jetpack Compose ViewPager 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 안드로이드 Jetpack Compose를 사용하여 ViewPager를 구현하는 방법을 살펴보았습니다. rememberPagerState를 사용하여 PagerState를 관리하고, HorizontalPager를 통해 페이지 스크롤을 구현할 수 있습니다. 이번 예제에서는 CustomViewPager 컴포저블 함수를 사용하여 더 유연하고 재사용 가능한 구조를 만들었습니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;상태&lt;/b&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt; 호이스팅 패턴을 사용하여 컴포저블의 상태를 상위 컴포저블로부터 전달받도록 했습니다. 이를 통해 컴포저블의 재사용성을 높이고, 다른 컴포저블과의 결합을 최소화할 수 있었습니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 방식으로 Jetpack Compose에서 ViewPager를 구현함으로써, 사용자에게 매력적인 스크롤 가능한 화면을 제공하면서도 유지보수와 확장성을 향상할 수 있고, 전통적인 xml 기반 개발에서의 복잡한 Adapter 설정과는 달리 Compose에서는 상대적으로 간단하고 직관적인 방식으로 구현할 수 있습니다.&lt;/p&gt;</description>
      <category>안드로이드/Compose UI</category>
      <category>Jetpack Compose</category>
      <category>viewpager</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/38</guid>
      <comments>https://code-tales.tistory.com/38#entry38comment</comments>
      <pubDate>Tue, 12 Mar 2024 11:30:46 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 트러블 슈팅] 서버 데이터 매핑 시, @SerializedName 어노테이션의 중요성</title>
      <link>https://code-tales.tistory.com/37</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 안드로이드 개발을 하다 보면 외부 API와의 통신은 필수적인 부분입니다. 이번 포스팅에서는 안드로이드 앱을 개발할 때, &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Retrofit을 사용하여 데이터를 받아오는 과정에서 JSON 형태의 서버 데이터를 data class를 이용해서 매핑 시, @SerializedName 어노테이션의 중요성에 대해 깨달은 트러블 슈팅 사례를 공유하고자 합니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 문제 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 3~4개월 정도 진행했을 때, 서버 통신 API가 약 30개 추가됐었습니다. 그에 맞춰서 서버 데이터를 매핑하는 DTO 클래스도 많이 생성돼 있었고, API 명세를 보면서 서버에서 반환하는 JSON 필드 이름과 안드로이드의 데이터 클래스 필드 이름을 동일하게 맞추어 개발했었습니다. @SerializedName 어노테이션을 사용하지 않았습니다.&lt;br /&gt;&lt;br /&gt;예를 들어, 서버에서 반환하는 JSON 응답이 아래와 같다고 가정하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1709902155195&quot; class=&quot;json&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;{
    &quot;userName&quot;: &quot;John Doe&quot;,
    &quot;userAge&quot;: 30
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이에 해당하는 DTO 클래스를 코틀린의 data class로 다음과 같이 작성했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1709902199228&quot; class=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;data class User(
    val userName: String,
    val userAge: Int
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이런 형태로 상당히 많은 양의 데이터 클래스가 생성돼 있었고, UI 영역에서는 해당 클래스의 필드를 직접 접근하는 형태로 개발이 돼있었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 발생한 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 중반에 서버의 JSON 응답 형식이 변경되었습니다. 서버 팀은 리팩토링 차원에서 필드명을 변경하고, 이를 저에게 전달했습니다. 예를 들어, userName 필드가 name으로 변경되고 userAge 필드가 age로 변경된 것입니다. 그래서 앱 내부에서도 많은 부분을 수정해야 하는 상황이 되었고, 특히 UI 영역에서 직접 필드에 접근했던 코드들이 모두 에러가 발생했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 해결 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;이 문제를 해결하기 위해서 @SerializedName 어노테이션을 사용했습니다. @SerializedName 어노테이션을 사용하면 서버의 JSON 필드명과 안드로이드 데이터 클래스의 필드명을 다르게 설정할 수 있어서, 서버에서 필드명을 변경해도 안드로이드 앱 코드는 큰 수정 없이 대응할 수 있었습니다.&lt;/b&gt;&lt;/span&gt; &lt;br /&gt;&lt;br /&gt;변경된 서버 응답에 대응하는 데이터 클래스는 다음과 같이 수정되었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1709902580352&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class User(
    @SerializedName(&quot;name&quot;)
    val userName: String,

    @SerializedName(&quot;age&quot;)
    val userAge: Int
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이러한 기능 덕분에 서버에서 필드명이 수정되어도 @SerializedName의 속성만 수정하면 되었고, UI 영역은 영향을 받지 않게 되었습니다. 즉, 서버의 JSON 응답 필드명이 변경되어도 Data 영역만 수정하면 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 안드로이드 @SerializedName 트러블 슈팅 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드에서 Retrofit을 사용하고, JSON 형태의 응답 데이터를 받기 위해 데이터 매핑 시, @SerializedName 어노테이션의 중요성에 대해 다시 한번 깨달았습니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;특히 서버 측에서의 필드명이 확정되지 않은 초기 개발 단계에서는 이러한 방법을 적극 활용하는 것이 좋다고 생각합니다. @SerializedName 어노테이션을 통해 서버 데이터 매핑 변경사항에 더욱 유연하게 대응하고, 불필요한 개발 리소스 낭비를 줄일 수 있기 때문입니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>안드로이드/트러블 슈팅</category>
      <category>안드로이드</category>
      <category>트러블슈팅</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/37</guid>
      <comments>https://code-tales.tistory.com/37#entry37comment</comments>
      <pubDate>Mon, 11 Mar 2024 11:30:05 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 트러블 슈팅] 빌드 시간 600초에서 30초로 단축한 사례</title>
      <link>https://code-tales.tistory.com/36</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 이번 포스팅에서는 안드로이드 빌드 시간을 약 600초에서 30초로 단축하는 데 성공한 트러블 슈팅 사례를 공유하겠습니다. 아래 사진은 안드로이드 스튜디오에서 확인할 수 있는 Build Analyzer입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;build_time.png&quot; data-origin-width=&quot;879&quot; data-origin-height=&quot;519&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbWoIP/btsFDWRQuw9/LbEXu15PZ1Uv3h5kdYPomK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbWoIP/btsFDWRQuw9/LbEXu15PZ1Uv3h5kdYPomK/img.png&quot; data-alt=&quot;안드로이드 빌드 시간이 약 600초에서 30초로 줄어든 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbWoIP/btsFDWRQuw9/LbEXu15PZ1Uv3h5kdYPomK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcbWoIP%2FbtsFDWRQuw9%2FLbEXu15PZ1Uv3h5kdYPomK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;안드로이드 빌드 시간이 약 600초에서 30초로 줄어든 모습&quot; loading=&quot;lazy&quot; width=&quot;669&quot; height=&quot;395&quot; data-filename=&quot;build_time.png&quot; data-origin-width=&quot;879&quot; data-origin-height=&quot;519&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;안드로이드 빌드 시간이 약 600초에서 30초로 줄어든 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 문제 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt; 안드로이드 실무에서 장기 프로젝트를 진행하면서 화면이 50~60개에 이르는 앱을 만들었습니다. 하지만, 프로젝트의 규모에 비례해서 빌드 시간이 점점 증가했고, 결국 약 600초까지 걸리는 상황에 이르렀습니다.&lt;/b&gt;&lt;/span&gt; 이는 개발자 경험을 크게 저하시켜서 신속한 개발과 문제 해결에 많은 어려움을 겪게 되었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 발생한 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;프로젝트의 낮은 Gradle 및 코틀린 버전이 이 문제의 주된 원인으로 밝혀졌습니다.&lt;/b&gt;&lt;/span&gt; 이 프로젝트는 5~6년 전에 생성되었기 때문에 개발 환경 구성 플러그인들의 버전이 오래되었고, 버전 업데이트 시 발생하는 빌드 에러로 인해 지속적으로 오래된 버전을 사용하고 있었기 때문입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 해결 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 다음과 같은 5가지 작업을 진행해서 빌드 시간을 대폭 줄일 수 있었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-1. Kotlin-Gradle-Plugin&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Kotlin-Gradle-Plugin 버전을 1.6.0에서 1.6.21로 업데이트했습니다.&lt;/b&gt;&lt;/span&gt; 높은 AGP 버전을 사용하기 위해 코틀린을 최신 버전으로 업데이트하고자 했으나 'kotlin-android-extensions' 플러그인을 사용으로 인해 1.6.21 버전으로 제한되었습니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;3-2. Android-Gradle-Plugin&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Android-Gradle-Plugin 버전을 2.3.2에서 7.0.2로 변경로 업데이트했습니다.&lt;/b&gt;&lt;/span&gt; 8.0.0 이상의 버전이 존재했지만 코틀린 버전 호환성으로 인해 7.0.2를 선택했습니다. Gradle 버전을 올리면서 새로운 기능을 사용할 수 있었는데, &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;'1-4 gradle.properties'&lt;/b&gt;&lt;/span&gt;에서 확인할 수 있습니다..&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;3-3. build.gradle (앱)&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;디버그 빌드는 프로덕트 빌드가 아니기 때문에 불필요한 생성 파일을 줄이기 위해서 buildTypes 내의 debug에 아래의 코드를 추가했습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;ext.alwaysUpdateBuildId = false&lt;/b&gt;&lt;br /&gt;ext.alwaysUpdateBuildId = false 옵션은 빌드 ID를 매번 업데이트하지 않도록 설정해서 빌드 시간을 단축하는 옵션입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;splits.abi.enable = false 및 splits.density.enable = false&lt;/b&gt;&lt;br /&gt;디버그 빌드를 수행할 경우에는 기능을 확인하는 것이 중요하므로 ABI 및 다양한 해상도에 대한 빌드를 생략했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;3-4. gradle.properties&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;gradle.properties 파일은 gralde을 사용한 프로젝트에서 빌드 환경을&lt;/span&gt; 설정하는 데 사용되는 파일입니다.&lt;/b&gt; 이 파일을 통해 빌드 프로세스에 대한 다양한 옵션을 지정할 수 있으며, 빌드 속도 향상, 메모리 최적화, 디버깅 등에 활용됩니다. 저는 gradle.properties에서 아래의 6가지 속성을 추가했습니다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;org.gradle.jvmargs=-Xmx6g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:MaxMetaspaceSize=1g --add-opens java.base/java.io=ALL-UNNAMED&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;org.gradle.configureondemand=true&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;org.gradle.configuration-cache=true (gradle 6 이상부터 사용 가능)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;android.enableBuildScriptClasspathCheck=false&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;android.defaults.buildfeatures.buildconfig=true&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;android.nonTransitiveRClass=false&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;android.nonFinalResIds=false&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;3-5. Gradle에서 Offline 모드 설정&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Gradle은 추가한 종속성(라이브러리, 플러그인)들에 대해서 매번 최신 버전을 확인하러 가는데 Offline 모드를 활성화하면&amp;nbsp; 이러한 과정을 생략해서 빌드시간이 단축됩니다.&lt;/b&gt;&lt;/span&gt; 단, 신규 종속성이 생기면 Offline모드를 해제해야 하는데, Offline 모드를 해제하지 않고 신규 종속성을 추가 및 빌드를 하면 로그에서 Offline 모드를 해제하라고 나옵니다.&lt;/p&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;4. 안드로이드 빌드 시간 트러블 슈팅 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에 소개한 5가지 방법으로 안드로이드에서 빌드 시간이 비정상적으로 길었던 트러블 슈팅을 진행할 수 있었습니다. 이러한 변경사항들은 프로젝트 빌드 시간을 크게 단축시켰으며, 개발 과정에서 개발자 경험을 향상할 수 있었습니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;장기 프로젝트를 진행하면 개발 환경도 지속적으로 관리하는 것이 중요하다는 점을 다시 한번 깨닫는 계기가 되었고, 이 글을 보신 분들께 도움이 됐으면 좋겠습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>안드로이드/트러블 슈팅</category>
      <category>안드로이드</category>
      <category>트러블슈팅</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/36</guid>
      <comments>https://code-tales.tistory.com/36#entry36comment</comments>
      <pubDate>Sun, 10 Mar 2024 11:30:15 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 Jetpack Compose Dialog 시리즈] RadioButton Dialog 구현하기 #8</title>
      <link>https://code-tales.tistory.com/34</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 이번 포스팅에서는 안드로이드 Jetpack Compose의 RadioButton을 사용해서 RadioButton Dialog를 구현하겠습니다. RadioButton의 특징은 다중 선택이 안되고, 한 가지만 선택할 수 있는 UI입니다. 그래서 일반적으로는 RadioGroup 등을 이용해서 RadioButton의 그룹을 만든 후, 한 가지만 선택되게 구현해 왔습니다. 그런데 Compose의 Material3 RadioButton에서는 RadioGroup은 없습니다. 어떤 형태로 구현할 수 있는지 예제를 통해 확인하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;radio_button.jpg&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;1544&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXzm4p/btsFzeiZXlr/cA40k3B27QEmUNygiR5vh1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXzm4p/btsFzeiZXlr/cA40k3B27QEmUNygiR5vh1/img.jpg&quot; data-alt=&quot;안드로이드 compose을 이용해서 만든 radio button dialog&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXzm4p/btsFzeiZXlr/cA40k3B27QEmUNygiR5vh1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXzm4p%2FbtsFzeiZXlr%2FcA40k3B27QEmUNygiR5vh1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;안드로이드 compose을 이용해서 만든 radio button dialog&quot; loading=&quot;lazy&quot; width=&quot;260&quot; height=&quot;558&quot; data-filename=&quot;radio_button.jpg&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;1544&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;안드로이드 compose을 이용해서 만든 radio button dialog&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. RadioButton Dialog 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 Compose의 Material3에서 제공하는 RadioButton을 활용해서 만든 RadioButon Dialog입니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;반복문을 이용해서 RadioButton을 보여주고 있고, RadioButton과 Text를 감싸고 있는 Row에 selectable()을 활용해서 클릭 됐을 때 모든 라디오 버튼의 체크를 해제하고, 현재 클릭한 라디오 버튼의 체크표시를 하고 있습니다.&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;RadioButton의 Label은 Text가 대체하고 있습니다.&lt;br /&gt;&lt;br /&gt;클릭했을 때 체크 표시가 정상적으로 반영되기 위해서 mutableStateOf&amp;lt;Boolean&amp;gt;으로 선언된 isChecked 변수를 바인딩 한 모습을 볼 수 있습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// CustomRadioButtonDialog.kt
@Composable
fun CustomRadioButtonDialog(
    initialRadioButtonList: ArrayList&amp;lt;RadioButtonState&amp;gt;?,
    onClickCancel: () -&amp;gt; Unit,
    onClickConfirm: (ArrayList&amp;lt;RadioButtonState&amp;gt;) -&amp;gt; Unit
) {
    val radioButtonList = initialRadioButtonList?.map {
        it.copy(
            text = it.text,
            isChecked = mutableStateOf(it.isChecked.value)
        )
    }?.let { ArrayList(it) } ?: ArrayList()

    Dialog(
        onDismissRequest = { onClickCancel() },
    ) {
        Card(
            shape = RoundedCornerShape(8.dp), // Card의 모든 꼭지점에 8.dp의 둥근 모서리 적용
        ) {
            Column(
                modifier = Modifier
                    .width(300.dp)
                    .wrapContentHeight()
                    .background(
                        color = Color.White,
                    )
                    .padding(16.dp),
                horizontalAlignment = Alignment.CenterHorizontally,
            ) {

                Text(text = &quot;후기를 입력해주세요.&quot;)

                Spacer(modifier = Modifier.height(15.dp))

                // radioButton list
                for (radioButtonState in radioButtonList) {
                    Row(
                        modifier = Modifier
                            .fillMaxWidth()
                            .selectable(
                                selected = radioButtonState.isChecked.value,
                                onClick = {
                                    radioButtonList.forEach {
                                        it.isChecked.value = false
                                    }
                                    radioButtonState.isChecked.value = true
                                }
                            ),
                        verticalAlignment = Alignment.CenterVertically,

                        ) {
                        RadioButton(
                            selected = radioButtonState.isChecked.value,
                            onClick = null
                        )

                        Spacer(modifier = Modifier.width(5.dp))

                        Text(
                            text = radioButtonState.text,
                            modifier = Modifier
                                .fillMaxWidth(0.8f)
                        )
                    }

                    Spacer(modifier = Modifier.height(5.dp))

                }

                Spacer(modifier = Modifier.height(15.dp))

                Row(
                    modifier = Modifier.fillMaxWidth(),
                    horizontalArrangement = Arrangement.Center,
                ) {
                    Button(onClick = {
                        onClickCancel()
                    }) {
                        Text(text = &quot;취소&quot;)
                    }

                    Spacer(modifier = Modifier.width(5.dp))

                    Button(onClick = {
                        onClickConfirm(radioButtonList)
                    }) {
                        Text(text = &quot;확인&quot;)
                    }
                }
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. RadioButton Dialog 사용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RadioButton Dialog를 사용하기 위해서 상태홀더 클래스를 정의했습니다. 해당 클래스에는 Dialog에 표시될 라디오 버튼들의 배열을 저장하고 있으며, 팝업창의 노출 여부 등의 데이터를 관리하고 있습니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;위의 코드에서 보았듯이 isChecked는 MutableState&amp;lt;Boolean&amp;gt; 타입으로 지정해서&amp;nbsp;상태가 변할 때마다 렌더링이 다시 되게 구현했습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// CustomRadioButtonDialogState.kt
data class CustomRadioButtonDialogState(
    var radioButtonList: ArrayList&amp;lt;RadioButtonState&amp;gt;? = null,
    var isShowDialog: Boolean = false,
    val onClickConfirm: (ArrayList&amp;lt;RadioButtonState&amp;gt;) -&amp;gt; Unit,
    val onClickCancel: () -&amp;gt; Unit,
)

data class RadioButtonState(
    val text: String,
    var isChecked: MutableState&amp;lt;Boolean&amp;gt; = mutableStateOf(false),
)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;MainViewModel에서는 상태홀더 클래스를 생성하고 저장합니다.&lt;/b&gt; &lt;/span&gt;UI를 담당하는 영역에서 해당 ViewModel의 상태홀더 클래스를 가져와서 팝업창의 노출 여부 및 현재 선택된 라디오 버튼 정보를 보여줄 예정입니다. showRadioButtonDialog() 함수는 MainScreen에서 버튼을 눌렀을 때 실행 될 함수입니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// MainViewModel.kt
@HiltViewModel
class MainViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle?,
) : ViewModel() {

    val customRadioButtonDialogState: MutableState&amp;lt;CustomRadioButtonDialogState?&amp;gt; =
        mutableStateOf(null)

    init {
        customRadioButtonDialogState.value = CustomRadioButtonDialogState(
            radioButtonList = arrayListOf(
                RadioButtonState(&quot;아주좋음&quot;, mutableStateOf(false)),
                RadioButtonState(&quot;좋음&quot;, mutableStateOf(false)),
                RadioButtonState(&quot;보통&quot;, mutableStateOf(false)),
                RadioButtonState(&quot;약간&quot;, mutableStateOf(false)),
                RadioButtonState(&quot;아주약간&quot;, mutableStateOf(false)),
            ),
            onClickConfirm = { radioButtonList -&amp;gt;
                customRadioButtonDialogState.value = customRadioButtonDialogState.value?.copy(
                    isShowDialog = false,
                    radioButtonList = radioButtonList
                )
            },
            onClickCancel = {
                customRadioButtonDialogState.value = customRadioButtonDialogState.value?.copy(
                    isShowDialog = false
                )
            }
        )
    }

    fun showRadioButtonDialog() {
        customRadioButtonDialogState.value =
            customRadioButtonDialogState.value?.copy(isShowDialog = true)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;MainScreen에서는 ViewModel에서 생성한 상태홀더 클래스를 가져와서 각 상태에 맞게 UI를 보여주게 구현했습니다. 버튼을 누르면 isShowDialog가 true가 돼서 팝업창이 보이고, 팝업창에서 확인을 누르면 RadioButtonList의 isChecked 변수가 변하게 돼서, 사용자가 선택한 옵션을 전달받을 수 있게 구현했습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// MainScreen.kt
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen(
    mainNavController: NavHostController,
    viewModel: MainViewModel
) {

    val customRadioButtonDialogState = viewModel.customRadioButtonDialogState.value

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text(text = &quot;메인화면&quot;) },
                navigationIcon = {
                    IconButton(onClick = { mainNavController.popBackStack() }) {
                        Icon(imageVector = Icons.Default.ArrowBack, contentDescription = null)
                    }
                }
            )
        }
    ) { paddingValues -&amp;gt;
        Column(
            modifier = Modifier.padding(paddingValues)
        ) {
            Button(
                onClick = { viewModel.showRadioButtonDialog() },
                shape = RectangleShape,
            ) {
                Text(
                    text = &quot;8. Show RadioButtonDialog&quot;,
                    textAlign = TextAlign.Center,
                    style = TextStyle(
                        fontSize = 14.sp,
                        fontWeight = FontWeight.Bold
                    )
                )
            }
            Text(
                text = &quot;text: ${viewModel.customRadioButtonDialogState.value?.radioButtonList?.map { it.isChecked.value }?.toString() ?: &quot;체크를 해보세요.&quot;}&quot;,
                style = TextStyle(
                    fontSize = 14.sp,
                    fontWeight = FontWeight.Bold
                )
            )

            if (customRadioButtonDialogState?.isShowDialog == true) {
                CustomRadioButtonDialog(
                    initialRadioButtonList = customRadioButtonDialogState.radioButtonList,
                    onClickCancel = customRadioButtonDialogState.onClickCancel,
                    onClickConfirm = customRadioButtonDialogState.onClickConfirm
                )
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. RadioButton Dialog 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 안드로이드 Jetpack Compose의 Material3에서 RadioButton을 활용하여 RadioButton Dialog를 구현하는 과정에 대해 알아봤습니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Dialog와 관련된 모든 데이터를 하나의 클래스에서 관리할 수 있도록 상태홀더 클래스를 구현하여 캡슐화를 진행했습니다. 이렇게 여러 가지 데이터를 하나의 변수로 선언하는 것보다 상태홀더 클래스 같은 패턴으로 데이터를 관리하면 책임이 분리되기 때문에 추후 유지보수 측면에서 유리하고 가독성 또한 좋아집니다.&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 시간에 알아본 내용을 토대로 여러분만의 RadioButton과 Dialog를 만드는데 도움이 됐으면 좋겠습니다. 다음 포스팅에서는 ViewPager, TabPager에 대해 포스팅할 예정입니다.&lt;/p&gt;</description>
      <category>안드로이드/Compose Dialog</category>
      <category>Dialog</category>
      <category>Jetpack Compose</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/34</guid>
      <comments>https://code-tales.tistory.com/34#entry34comment</comments>
      <pubDate>Sat, 9 Mar 2024 11:30:54 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 트러블 슈팅] 인앱 다국어 설정 버그 Android 13, Android 14</title>
      <link>https://code-tales.tistory.com/35</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 이번 포스팅에서는 안드로이드 개발 실무에서 겪었던 이슈와 해결과정에 대해 적으려고 합니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;제가 개발 중인 앱은 앱 내에 언어를 설정할 수 있는 기능이 있었습니다. 지원하는 언어는 한국어, 아랍어, 영어로 총 3개였는데 몇 년째 문제가 없다가 최근에 다국어 설정이 제대로 되지 않는다는 연락을 받았습니다.&lt;/b&gt;&lt;/span&gt; 분석한 결과, 안드로이드 13과 안드로이드 14에서 발생하는 이슈였습니다. 그런데 이상하게도 여러 가지 언어가 섞여서 보인다는 점이었습니다. 예를 들어, 영어로 설정했을 때 한국어와 영어가 섞여서 보이는 현상이 있었습니다. 기나긴 사투 끝에 결국 해결을 했고 그 과정에 대해서 기록하고자 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 안드로이드 인앱 다국어 기능&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드는 다국어를 지원하기 위해서 res 폴더 내에 values를 언어별로 설정할 수 있습니다. 이렇게 설정을 하면 안드로이드 OS 설정화면에서 설정한 언어에 따라서 사용하는 string.xml이 결정됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;249&quot; data-origin-height=&quot;206&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bATBwo/btsFDGAlB4I/GOoDG1NLIzyiHi6og99j9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bATBwo/btsFDGAlB4I/GOoDG1NLIzyiHi6og99j9k/img.png&quot; data-alt=&quot;언어별로 string.xml을 생성한 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bATBwo/btsFDGAlB4I/GOoDG1NLIzyiHi6og99j9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbATBwo%2FbtsFDGAlB4I%2FGOoDG1NLIzyiHi6og99j9k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;언어별로 string.xml을 생성한 모습&quot; loading=&quot;lazy&quot; width=&quot;222&quot; height=&quot;184&quot; data-origin-width=&quot;249&quot; data-origin-height=&quot;206&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;언어별로 string.xml을 생성한 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그런데 제가 개발한 앱은 인앱에서 언어를 직접 설정하는 기능이 있었습니다. Activity의 attachBaseContext() 함수를 오버라이딩해서 context를 전달받는 아래와 같은 함수를 만들어서 활용하고 있었습니다. 안드로이드 12까진 정상적으로 작동했습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;private fun updateResources(context: Context, language: String?, country: String?): Context {
    val locale = Locale(language, country)
    Locale.setDefault(locale)

    val configuration = context.resources.configuration
    configuration.setLocale(locale)
    configuration.setLayoutDirection(locale)

    try {
        // 일부 언어변경이 정상적으로 적용되지 않는 현상 수정
        context.resources.updateConfiguration(configuration, context.resources.displayMetrics)
    } catch (e: Exception) {
        e.printStackTrace()
    }
    return context.createConfigurationContext(configuration)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 안드로이드 13&amp;amp;14에서 발생한 문제와 해결과정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 13 이상부터는 앱마다 사용할 언어를 설정할 수 있는 기능이 추가되었습니다. 안드로이드 14 기준으로 '설정 &amp;gt;&amp;nbsp; 일반 &amp;gt; 앱 언어' 메뉴를 통해 설정할 수 있게 됐습니다. 그래서 이 기능을 컨트롤할 수 있는 함수가 추가되었습니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;AppCompat 1.6.0 이상에서만 사용 가능한 setApplicationLocales()와 getApplicationLocales()입니다. 언어를 변경했을 때 해당 함수를 사용해서 문제를 해결했습니다.&lt;/b&gt;&lt;/span&gt; &lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 예시 코드를 보면 안드로이드 버전 13 이상에서만 작동하며, 사용자가 선택한 언어에 따라서 LocaleListCompat.forLanguageTags() 함수가 반환한 값을 setApplicationLocales() 함수의 매개변수로 전달하고 있습니다. 그리고 앱을 재시작하는 코드를 넣음으로써 언어를 정상적으로 전환할 수 있었습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;if (Build.VERSION.SDK_INT &amp;gt;= Build.VERSION_CODES.TIRAMISU) {
    val appLocale = when (yourLanguageCode) {
        &quot;MY_CODE_EN&quot; -&amp;gt; {
            LocaleListCompat.forLanguageTags(&quot;en&quot;)
        }
        &quot;MY_CODE_KO&quot; -&amp;gt; {
            LocaleListCompat.forLanguageTags(&quot;ko&quot;)
        }
        &quot;MY_CODE_AR&quot; -&amp;gt; {
            LocaleListCompat.forLanguageTags(&quot;ar&quot;)
        }
        else -&amp;gt; {
            LocaleListCompat.forLanguageTags(&quot;ko&quot;)
        }
    }
    // Call this on the main thread as it may require Activity.restart()
    AppCompatDelegate.setApplicationLocales(appLocale)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 안드로이드 13&amp;amp;14 인앱 다국어 설정 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅을 통해 안드로이드 13, 14에서 발생한 인앱 다국어 설정 문제와 그 해결 방법을 살펴보았습니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;기존에는 Activity의 'attachBaseContext()' 함수를 오버라이딩하고 'context.&lt;span style=&quot;color: #0d0d0d; text-align: start;&quot;&gt;createConfigurationContext()' 함수를 사용해서 언어 설정을 지원했었는데 안드로이드 13 이상 버전에서는 정상적으로 적용되지 않았습니다. 그래서 AppCompat 1.6.0 이상 버전에서 제공하는 setApplicationLocales() 함수를 사용해서 앱 내의 언어 설정을 제어할 수 있게 되었습니다.&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;안드로이드는 시스템의 변화에 따라 기존에 잘 작동하던 코드도 문제가 발생할 수 있으므로 최신 안드로이드 버전에 대한 지속적인 관심, 공부, 테스트가 필요함을 다시 한번 느낄 수 있었습니다. 이러한 경험들을 공유함으로써 다른 분들께도 도움이 되었으면 좋겠습니다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>안드로이드/트러블 슈팅</category>
      <category>안드로이드</category>
      <category>트러블슈팅</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/35</guid>
      <comments>https://code-tales.tistory.com/35#entry35comment</comments>
      <pubDate>Fri, 8 Mar 2024 11:30:27 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 Jetpack Compose Dialog 시리즈] Checkbox Dialog 구현하기 #7</title>
      <link>https://code-tales.tistory.com/33</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 이번 포스팅에서는 안드로이드 Jetpack Compose에서 Checkbox를 보여주는 Dialog를 만들겠습니다. Compose의 기본 작동방식에 대해 더 초점이 맞춰져 있기 때문에 체크박스를 어떻게 만들고, 각 체크 상태를 어떤 식으로 처리해야 하는지를 중점적으로 설명하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;check_box.jpg&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;1544&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4yhdD/btsFsoNWjWV/0mkbHF5llxcRo5IKkXTC50/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4yhdD/btsFsoNWjWV/0mkbHF5llxcRo5IKkXTC50/img.jpg&quot; data-alt=&quot;안드로이드 compose을 이용해서 만든 checkbox dialog&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4yhdD/btsFsoNWjWV/0mkbHF5llxcRo5IKkXTC50/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4yhdD%2FbtsFsoNWjWV%2F0mkbHF5llxcRo5IKkXTC50%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;안드로이드 compose을 이용해서 만든 checkbox dialog&quot; loading=&quot;lazy&quot; width=&quot;212&quot; height=&quot;455&quot; data-filename=&quot;check_box.jpg&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;1544&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;안드로이드 compose을 이용해서 만든 checkbox dialog&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;1. Checkbox Dialog 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compose Material3에서 제공하는 Checkbox를 사용해서 Dialog를 만들 예정입니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;웹 개발을 해보신 분들이라면 Checkbox에는 텍스트를 표시하는 label도 필요하다고 생각할 수 있지만, 안드로이드 Compose에서는 Row를 이용해서 Checkbox와 Text를 배치합니다.&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;아래 코드를 보면 어떤 식으로 label을 표현하는지 확인할 수 있습니다. &lt;br /&gt;&lt;br /&gt;또한, 터치를 했을 때 Row의 clickable를 활용해서 각 상태를 변경하도록 구현했습니다. 왜냐하면 체크박스 UI와 Text UI 둘 중 어느 곳을 터치해도 체크박스의 상태가 반영되기를 원했기 때문입니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// CustomCheckboxDialog.kt
@Composable
fun CustomCheckboxDialog(
    initialCheckboxList: ArrayList&amp;lt;CheckboxState&amp;gt;?,
    onClickCancel: () -&amp;gt; Unit,
    onClickConfirm: (ArrayList&amp;lt;CheckboxState&amp;gt;) -&amp;gt; Unit
) {
    val checkboxList = initialCheckboxList?.map {
        it.copy(
            text = it.text,
            isChecked = mutableStateOf(it.isChecked.value)
        )
    }?.let { ArrayList(it) } ?: ArrayList()

    Dialog(
        onDismissRequest = { onClickCancel() },
    ) {
        Card(
            shape = RoundedCornerShape(8.dp), // Card의 모든 꼭지점에 8.dp의 둥근 모서리 적용
        ) {
            Column(
                modifier = Modifier
                    .width(300.dp)
                    .wrapContentHeight()
                    .background(
                        color = Color.White,
                    )
                    .padding(16.dp),
                horizontalAlignment = Alignment.CenterHorizontally,
            ) {

                Text(text = &quot;추가 옵션을 선택해주세요.&quot;)

                Spacer(modifier = Modifier.height(15.dp))

                // checkbox list
                for (checkboxState in checkboxList) {
                    Row(
                        modifier = Modifier
                            .fillMaxWidth()
                            .clickable {
                                checkboxState.isChecked.value = !checkboxState.isChecked.value
                            },
                        verticalAlignment = Alignment.CenterVertically,

                        ) {
                        Checkbox(
                            checked = checkboxState.isChecked.value,
                            onCheckedChange = null
                        )

                        Spacer(modifier = Modifier.width(5.dp))

                        Text(
                            text = checkboxState.text,
                            modifier = Modifier
                                .fillMaxWidth(0.8f)
                        )
                    }

                    Spacer(modifier = Modifier.height(5.dp))

                }

                Spacer(modifier = Modifier.height(15.dp))

                Row(
                    modifier = Modifier.fillMaxWidth(),
                    horizontalArrangement = Arrangement.Center,
                ) {
                    Button(onClick = {
                        onClickCancel()
                    }) {
                        Text(text = &quot;취소&quot;)
                    }

                    Spacer(modifier = Modifier.width(5.dp))

                    Button(onClick = {
                        onClickConfirm(checkboxList)
                    }) {
                        Text(text = &quot;확인&quot;)
                    }
                }
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Checkbox Dialog 사용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CheckBox Dialog를 편하게 사용하기 위해서 상태홀더 클래스를 만들었습니다. Dialog에 표시할 CheckboxList 및 Dialog 표시 여부 등 Dialog에 관련된 정보를 저장합니다. CheckboxState의 text는 Checkbox의 label 역할을 하기 위해서 만들었습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// CustomCheckboxDialogState.kt
data class CustomCheckboxDialogState(
    var checkboxList: ArrayList&amp;lt;CheckboxState&amp;gt;? = null,
    var isShowDialog: Boolean = false,
    val onClickConfirm: (ArrayList&amp;lt;CheckboxState&amp;gt;) -&amp;gt; Unit,
    val onClickCancel: () -&amp;gt; Unit,
)

data class CheckboxState(
    val text: String,
    var isChecked: MutableState&amp;lt;Boolean&amp;gt; = mutableStateOf(false),
)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;MainViewModel에서는 상태홀더 클래스를 생성하고 초기값을 저장합니다. CheckboxState의 isChecked는 mutableStateOf 타입으로 지정해서 값이 변할 때마다 렌더링을 새로 그려줍니다. 그래서 사용자가 터치했을 때 체크표시가 바뀌게 됩니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// MainViewModel.kt
@HiltViewModel
class MainViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle?,
) : ViewModel() {
   
    val customCheckboxDialogState: MutableState&amp;lt;CustomCheckboxDialogState?&amp;gt; =
        mutableStateOf(null)

    init {
        customCheckboxDialogState.value = CustomCheckboxDialogState(
            checkboxList = arrayListOf(
                CheckboxState(&quot;치킨무&quot;, mutableStateOf(false)),
                CheckboxState(&quot;어니언 양파링&quot;, mutableStateOf(false)),
                CheckboxState(&quot;치즈스틱&quot;, mutableStateOf(false)),
                CheckboxState(&quot;떡볶이&quot;, mutableStateOf(false)),
                CheckboxState(&quot;소떡소떡&quot;, mutableStateOf(false)),
            ),
            onClickConfirm = { checkboxList -&amp;gt;
                customCheckboxDialogState.value = customCheckboxDialogState.value?.copy(
                    isShowDialog = false,
                    checkboxList = checkboxList
                )
            },
            onClickCancel = {
                customCheckboxDialogState.value = customCheckboxDialogState.value?.copy(
                    isShowDialog = false
                )
            }
        )
    }

    fun showCheckboxDialog() {
        customCheckboxDialogState.value =
            customCheckboxDialogState.value?.copy(isShowDialog = true)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;MainScreen에서는 MainViewModel에서 만든 상태홀더 클래스를 통해서 Dialog를 보여주는 기능을 구현했습니다. 버튼을 누르면 viewModel.showCheckboxDialog()를 호출해서 상태홀더 클래스의 isShowDialog를 true로 변경해 주고, 이를 Composable 함수에서 감지하여 위에서 구현한 CustomCheckboxDialog가 팝업창으로 나오게 됩니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// MainScreen.kt
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen(
    mainNavController: NavHostController,
    viewModel: MainViewModel
) {
    val customCheckboxDialogState = viewModel.customCheckboxDialogState.value

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text(text = &quot;메인화면&quot;) },
                navigationIcon = {
                    IconButton(onClick = { mainNavController.popBackStack() }) {
                        Icon(imageVector = Icons.Default.ArrowBack, contentDescription = null)
                    }
                }
            )
        }
    ) { paddingValues -&amp;gt;
        Column(
            modifier = Modifier.padding(paddingValues)
        ) {
            Button(
                onClick = { viewModel.showCheckboxDialog() },
                shape = RectangleShape,
            ) {
                Text(
                    text = &quot;7. Show CheckboxDialog&quot;,
                    textAlign = TextAlign.Center,
                    style = TextStyle(
                        fontSize = 14.sp,
                        fontWeight = FontWeight.Bold
                    )
                )
            }
            Text(
                text = &quot;text: ${viewModel.customCheckboxDialogState.value?.checkboxList?.map { it.isChecked.value }?.toString() ?: &quot;체크를 해보세요.&quot;}&quot;,
                style = TextStyle(
                    fontSize = 14.sp,
                    fontWeight = FontWeight.Bold
                )
            )

            if (customCheckboxDialogState?.isShowDialog == true) {
                CustomCheckboxDialog(
                    initialCheckboxList = customCheckboxDialogState.checkboxList,
                    onClickCancel = customCheckboxDialogState.onClickCancel,
                    onClickConfirm = customCheckboxDialogState.onClickConfirm
                )
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Checkbox Dialog 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 안드로이드 Jetpack Compose의 Material3에서 Checkbox를 활용하여 Checkbox Dialog를 만들었습니다. 오늘 예제로 구현한 것처럼 Compose의 Material3을 사용하면 xml에서 복잡하게 구현했던 UI를 쉽고 편리하게 구현할 수 있는 장점이 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, 선언형 UI 프로그래밍은 상태 관리를 통해서 렌더링, 리렌더링을 진행하기 때문에 상태 관리에 대한 이해가 중요하고, 무분별하게 상태를 선언하면 그만큼 렌더링의 최적화에 문제가 생기기 때문에 조심해서 다뤄야 합니다.&lt;br /&gt;&lt;br /&gt;다음 포스팅에서는 RadioButton을 사용한 Dialog를 구현하겠습니다.&lt;/p&gt;</description>
      <category>안드로이드/Compose Dialog</category>
      <category>Dialog</category>
      <category>Jetpack Compose</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/33</guid>
      <comments>https://code-tales.tistory.com/33#entry33comment</comments>
      <pubDate>Thu, 7 Mar 2024 11:30:06 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 Jetpack Compose Dialog 시리즈] BasicTextField Dialog 구현하기 #6</title>
      <link>https://code-tales.tistory.com/32</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 이번 포스팅에서는 안드로이드 Jetpack Compose에서 Text를 입력할 수 있는 BasicTextField를 사용해서 Dialog를 구현하겠습니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Compose에서는 TextField와 BasicTextField가 있는데 디자인을 커스텀하기 위해서는 BasicTextField를 사용하는 것이 유리하기 때문에 BasicTextField로 Dialog를 만들겠습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;text_field_dialog.jpg&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;1544&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/r0XgD/btsFoGntaZR/k8W66XnTEqokLSNOwRGHpk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/r0XgD/btsFoGntaZR/k8W66XnTEqokLSNOwRGHpk/img.jpg&quot; data-alt=&quot;안드로이드 compose을 이용해서 만든 text field dialog&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/r0XgD/btsFoGntaZR/k8W66XnTEqokLSNOwRGHpk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fr0XgD%2FbtsFoGntaZR%2Fk8W66XnTEqokLSNOwRGHpk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;안드로이드 compose을 이용해서 만든 text field dialog&quot; loading=&quot;lazy&quot; width=&quot;237&quot; height=&quot;508&quot; data-filename=&quot;text_field_dialog.jpg&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;1544&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;안드로이드 compose을 이용해서 만든 text field dialog&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. BasicTextField Dialog 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compose Material3에서 제공하는 Dialog와 BasicTextField를 사용해서 TextFieldDialog를 만들었습니다. text를 표시하기 위해서 TextState를 관리하는 변수를 만들고, 해당 변수를 BasicTextField의 value와 onValueChange에서 활용해야 합니다. 이렇게 해야 사용자가 입력한 텍스트가 실시간으로 UI에 표시되기 때문입니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;아래 코드를 보면 BasicTextField에서 decorationBox를 재정의하는 것을 확인할 수 있습니다. decorationBox를 통해서 TextField의 디자인을 원하는 형태로 수정할 수 있기 때문입니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// CustomTextFieldDialog.kt
@Composable
fun CustomTextFieldDialog(
    initialText: String?,
    onClickCancel: () -&amp;gt; Unit,
    onClickConfirm: (text: String) -&amp;gt; Unit
) {

    val text = remember { mutableStateOf(initialText ?: &quot;&quot;) }

    Dialog(
        onDismissRequest = { onClickCancel() },
    ) {
        Card(
            shape = RoundedCornerShape(8.dp), // Card의 모든 꼭지점에 8.dp의 둥근 모서리 적용
        ) {
            Column(
                modifier = Modifier
                    .width(300.dp)
                    .wrapContentHeight()
                    .background(
                        color = Color.White,
                    )
                    .padding(16.dp),
                horizontalAlignment = Alignment.CenterHorizontally,
            ) {

                Text(text = &quot;후기를 입력해주세요.&quot;)

                Spacer(modifier = Modifier.height(15.dp))

                // TextField
                BasicTextField(
                    value = text.value,
                    onValueChange = { text.value = it },
                    singleLine = true,
                    textStyle = TextStyle(
                        color = Color.Black,
                        fontSize = 16.sp,
                        fontWeight = FontWeight.Normal,
                    ),
                    decorationBox = { innerTextField -&amp;gt;
                        Row(
                            modifier = Modifier
                                .fillMaxWidth()
                                .background(
                                    color = Color(0xFFBAE5F5),
                                    shape = RoundedCornerShape(size = 16.dp)
                                )
                                .padding(all = 16.dp),
                            verticalAlignment = Alignment.CenterVertically,
                        ) {
                            Icon(
                                imageVector = Icons.Default.Create,
                                contentDescription = &quot;&quot;,
                                tint = Color.DarkGray,
                            )
                            Spacer(modifier = Modifier.width(width = 8.dp))
                            innerTextField()
                        }
                    },
                )

                Spacer(modifier = Modifier.height(15.dp))

                Row(
                    modifier = Modifier.fillMaxWidth(),
                    horizontalArrangement = Arrangement.Center,
                ) {
                    Button(onClick = {
                        onClickCancel()
                    }) {
                        Text(text = &quot;취소&quot;)
                    }

                    Spacer(modifier = Modifier.width(5.dp))

                    Button(onClick = {
                        onClickConfirm(text.value)
                    }) {
                        Text(text = &quot;확인&quot;)
                    }
                }
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. BasicTextField Dialog 사용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 만든 BasicTextFieldDialog를 사용하기 위해서 상태를 관리하는 클래스를 만든 후, Screen과 ViewModel에서 비즈니스 로직과 UI 영역을 만들었습니다.&lt;br /&gt;&lt;br /&gt;CustomTextFieldDialogState에서는 사용자가 입력한 text를 저장하고 있으며, Dialog에 관련된 기능을 모두 포함하게 구현했습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// CustomTextFieldDialogState.kt
data class CustomTextFieldDialogState(
    var text: String? = null,
    var isShowDialog: Boolean = false,
    val onClickConfirm: (text: String) -&amp;gt; Unit,
    val onClickCancel: () -&amp;gt; Unit,
)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;아래 코드는 ViewModel입니다. 위에서 만든 상태 관련 클래스를 MutableState에 저장하고 있으며, Dialog에서 확인을 눌렀을 때 사용자가 입력한 Text를 저장하기 위해서 onClickConfirm 및 onClickCancel 함수에서 text와 isShowDialog에 대한 처리를 하고 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// MainViewModel.kt
@HiltViewModel
class MainViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle?,
) : ViewModel() {

    val customTextFieldDialogState: MutableState&amp;lt;CustomTextFieldDialogState?&amp;gt; =
        mutableStateOf(null)

    init {
        customTextFieldDialogState.value = CustomTextFieldDialogState(
            onClickConfirm = { text -&amp;gt;
                customTextFieldDialogState.value = customTextFieldDialogState.value?.copy(
                    isShowDialog = false,
                    text = text
                )
            },
            onClickCancel = {
                customTextFieldDialogState.value = customTextFieldDialogState.value?.copy(
                    isShowDialog = false
                )
            }
        )
    }

    fun showTextFieldDialog() {
        customTextFieldDialogState.value =
            customTextFieldDialogState.value?.copy(isShowDialog = true)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;UI를 보여주는 MainScreen 코드입니다. isShowDialog 변수를 통해서 BasicTextFieldDialog 노출 여부를 결정하고 있으며, 사용자가 입력한 텍스트를 버튼 아래에 배치하여 보여주고 있습니다. 사용하려는 목적에 맞게 수정해서 활용하면 좋을 것 같습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// MainScreen.kt
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen(
    mainNavController: NavHostController,
    viewModel: MainViewModel
) {

    val customTextFieldDialogState = viewModel.customTextFieldDialogState.value

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text(text = &quot;메인화면&quot;) },
                navigationIcon = {
                    IconButton(onClick = { mainNavController.popBackStack() }) {
                        Icon(imageVector = Icons.Default.ArrowBack, contentDescription = null)
                    }
                }
            )
        }
    ) { paddingValues -&amp;gt;
        Column(
            modifier = Modifier.padding(paddingValues)
        ) {
            Button(
                onClick = { viewModel.showTextFieldDialog() },
                shape = RectangleShape,
            ) {
                Text(
                    text = &quot;6. Show TextFieldDialog&quot;,
                    textAlign = TextAlign.Center,
                    style = TextStyle(
                        fontSize = 14.sp,
                        fontWeight = FontWeight.Bold
                    )
                )
            }
            Text(
                text = &quot;text: ${viewModel.customTextFieldDialogState.value?.text ?: &quot;텍스트를 입력해주세요.&quot;}&quot;,
                style = TextStyle(
                    fontSize = 14.sp,
                    fontWeight = FontWeight.Bold
                )
            )

            if (customTextFieldDialogState?.isShowDialog == true) {
                CustomTextFieldDialog(
                    initialText = customTextFieldDialogState.text,
                    onClickCancel = customTextFieldDialogState.onClickCancel,
                    onClickConfirm = customTextFieldDialogState.onClickConfirm
                )
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. BasicTextField Dialog 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 안드로이드 Jetpack Compose에서 BasicTextFieldDialog를 구현하고 사용하는 방법에 대해서 알아봤습니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;구현하는데 어려움은 없었지만 실제로 개발할 때는 TextField의 디자인을 커스텀해야 하는 경우가 많습니다. 그래서 BasicTextField를 사용했으며, decorationBox를 재정의해서 TextField의 디자인을 변경할 수&amp;nbsp;있었습니다.&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;이번 과정을 통해서 여러분이 제작하고자 하는 Dialgo와 TextField를 커스텀하는데 도움이 됐으면 좋겠습니다.&lt;/p&gt;</description>
      <category>안드로이드/Compose Dialog</category>
      <category>Dialog</category>
      <category>Jetpack Compose</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/32</guid>
      <comments>https://code-tales.tistory.com/32#entry32comment</comments>
      <pubDate>Wed, 6 Mar 2024 11:30:55 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 Jetpack Compose Dialog 시리즈] TimePicker Dialog 구현하기 #5</title>
      <link>https://code-tales.tistory.com/31</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 이번 포스팅에서는 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;안드로이드 Jetpack Compose에서 TimePickerDialog를 구현하겠습니다.&lt;/b&gt;&lt;/span&gt; TimePickerDialog는 시간을 설정할 때 유용하게 사용할 수 있는 Dialog입니다. Compose Material3에서 제공하는 TimePicker를 사용해서 아래 사진과 같은 Dialog를 만들겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;time_picker.jpg&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;1544&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xJOzy/btsFmjM8mHe/dIDYZUck9MgQDycU4Ld88k/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xJOzy/btsFmjM8mHe/dIDYZUck9MgQDycU4Ld88k/img.jpg&quot; data-alt=&quot;안드로이드 compose material3을 이용해서 만든 time picker dialog&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xJOzy/btsFmjM8mHe/dIDYZUck9MgQDycU4Ld88k/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxJOzy%2FbtsFmjM8mHe%2FdIDYZUck9MgQDycU4Ld88k%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;안드로이드 compose material3을 이용해서 만든 time picker dialog&quot; loading=&quot;lazy&quot; width=&quot;253&quot; height=&quot;543&quot; data-filename=&quot;time_picker.jpg&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;1544&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;안드로이드 compose material3을 이용해서 만든 time picker dialog&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. TimePicker Dialog 만들기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compose Material3에서 제공하는 Dialog와 TimePicker를 사용해서 TimePickerDialog를 만들었습니다. Compose는 상태를 관리하기 때문에 rememberTimePickerState를 통해 초기값이나 24시간제와 같은 옵션을 설정할 수 있습니다. 해당 상태변수를 TimePicker의 state 매개변수로 전달하면 됩니다.&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;TimePickerState로 전달하는 initialHour의 경우에는 24시간제에 해당하기 때문에 0~23 사이의 값을 전달해야 합니다. 만약에 12시간 제로 변수를 관리하고 있다면 오후의 경우에 +12를 해주어야 합니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// CustomTimePickerDialog.kt
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CustomTimePickerDialog(
    selectedHour: Int?,
    selectedMinute: Int?,
    onClickCancel: () -&amp;gt; Unit,
    onClickConfirm: (hour: Int, minute: Int) -&amp;gt; Unit
) {
    val timePickerState = rememberTimePickerState(
        initialHour = selectedHour ?: 0,
        initialMinute = selectedMinute ?: 0,
        is24Hour = false
    )
    Dialog(
        onDismissRequest = { onClickCancel() },
    ) {
        Card(
            shape = RoundedCornerShape(8.dp), // Card의 모든 꼭지점에 8.dp의 둥근 모서리 적용
        ) {
            Column(
                modifier = Modifier
                    .width(300.dp)
                    .wrapContentHeight()
                    .background(
                        color = Color.White,
                    )
                    .padding(16.dp),
                horizontalAlignment = Alignment.CenterHorizontally,
            ) {

                Text(text = &quot;시간을 선택해주세요.&quot;)

                Spacer(modifier = Modifier.width(15.dp))

                TimePicker(
                    state = timePickerState,
                    modifier = Modifier.padding(top = 10.dp)
                )

                Row(
                    modifier = Modifier.fillMaxWidth(),
                    horizontalArrangement = Arrangement.Center,
                ) {
                    Button(onClick = {
                        onClickCancel()
                    }) {
                        Text(text = &quot;취소&quot;)
                    }

                    Spacer(modifier = Modifier.width(5.dp))

                    Button(onClick = {
                        val hour = timePickerState.hour
                        val minute = timePickerState.minute
                        onClickConfirm(hour, minute)
                    }) {
                        Text(text = &quot;확인&quot;)
                    }
                }
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. TimePicker Dialog 사용하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 만든 TimePickerDialog를 사용하기 위해서 관련된 상태를 만들어주고, 상태에 따른 Dialog의 노출 여부를 설정하겠습니다.&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;CustomTimePickerDialogState에서는 선택한 Hour와 Minute 정보를 가지고 있으며, selectedHHmm라는 getter를 통해 HHmm 형태로 받을 수 있게 구현했습니다.&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// CustomTimePickerDialogState.kt
data class CustomTimePickerDialogState(
    var selectedHour: Int? = null,
    var selectedMinute: Int? = null,
    var isShowDialog: Boolean = false,
    val onClickConfirm: (hour: Int, minute: Int) -&amp;gt; Unit,
    val onClickCancel: () -&amp;gt; Unit,
) {
    val selectedHHmm: String?
        get() {
            return if (selectedHour != null &amp;amp;&amp;amp; selectedMinute != null) {
                val time = String.format(&quot;%02d%02d&quot;, selectedHour, selectedMinute)
                time
            } else {
                null
            }
        }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;아래는 ViewModel의 내부 모습입니다. 위에서 만든 상태 관련 클래스를 MutableState를 통해 관리하고 있으며, 클릭 이벤트에 대한 처리를 하고 있습니다. Confirm과 Cancel 이벤트에서는 각각 isShowDialog에 false를 전달하고 selectedHour와 selectedMinute에 대한 처리를 하고 있습니다. 또한, showTimePickerDialog 함수에서는 isShowDialog에 true를 전달함으로써 Dialog가 나타나게 됩니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// MainViewModel.kt
@HiltViewModel
class MainViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle?,
) : ViewModel() {

    val customTimePickerDialogState: MutableState&amp;lt;CustomTimePickerDialogState?&amp;gt; =
        mutableStateOf(null)

    init {
        customTimePickerDialogState.value = CustomTimePickerDialogState(
            onClickConfirm = { hour, minute -&amp;gt;
                customTimePickerDialogState.value = customTimePickerDialogState.value?.copy(
                    isShowDialog = false,
                    selectedHour = hour,
                    selectedMinute = minute
                )
            },
            onClickCancel = {
                customTimePickerDialogState.value = customTimePickerDialogState.value?.copy(
                    isShowDialog = false
                )
            }
        )
    }

    fun showTimePickerDialog() {
        customTimePickerDialogState.value =
            customTimePickerDialogState.value?.copy(isShowDialog = true)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;아래는 화면을 담당하는 MainScreen입니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;사용자가 버튼을 누르면 ViewModel에서 정의한 showTimePickerDialog()를 호출합니다. 그러면 isShowDialog에 true가 전달되면서 CustomTimePickerDialog가 화면에 보이게 됩니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// MainScreen.kt
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen(
    mainNavController: NavHostController,
    viewModel: MainViewModel
) {

    val customTimePickerDialogState = viewModel.customTimePickerDialogState.value

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text(text = &quot;메인화면&quot;) },
                navigationIcon = {
                    IconButton(onClick = { mainNavController.popBackStack() }) {
                        Icon(imageVector = Icons.Default.ArrowBack, contentDescription = null)
                    }
                }
            )
        }
    ) { paddingValues -&amp;gt;
        Column(
            modifier = Modifier.padding(paddingValues)
        ) {
           
            Button(
                onClick = { viewModel.showTimePickerDialog() },
                shape = RectangleShape,
            ) {
                Text(
                    text = &quot;5. Show TimePickerDialog&quot;,
                    textAlign = TextAlign.Center,
                    style = TextStyle(
                        fontSize = 14.sp,
                        fontWeight = FontWeight.Bold
                    )
                )
            }
            Text(
                text = &quot;Selected Time: ${viewModel.customTimePickerDialogState.value?.selectedHHmm ?: &quot;시간을 선택해주세요.&quot;}&quot;,
                style = TextStyle(
                    fontSize = 14.sp,
                    fontWeight = FontWeight.Bold
                )
            )

            if (customTimePickerDialogState?.isShowDialog == true) {
                CustomTimePickerDialog(
                    selectedHour = customTimePickerDialogState.selectedHour,
                    selectedMinute = customTimePickerDialogState.selectedMinute,
                    onClickCancel = customTimePickerDialogState.onClickCancel,
                    onClickConfirm = customTimePickerDialogState.onClickConfirm
                )
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. TimePicker Dialog 정리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 안드로이드 Jetpack Compose Material3 디자인에서 Dialog와 TimePicker를 사용해서 TimePickerDialog를 구현했습니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;TimePicker에서 주의할 점은 Hour(시간) 처리에 대한 부분입니다. 시간은 24시간제와 12시간 제로 나뉘기 때문에 12시간 제로 데이터를 관리하고 있다면 12를 더한 값을 TimePicker의 초기 상태값으로 전달해야만 합니다.&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 만든 CustomTimePickerDialogState에서는 24시간 제로 데이터를 관리하고 있는데, 필요에 따라서 12시간 제로 시간을 반환해 주는 getter를 만들어서 사용해도 좋을 것 같습니다.&lt;/p&gt;</description>
      <category>안드로이드/Compose Dialog</category>
      <category>Dialog</category>
      <category>Jetpack Compose</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/31</guid>
      <comments>https://code-tales.tistory.com/31#entry31comment</comments>
      <pubDate>Tue, 5 Mar 2024 11:30:35 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 Jetpack Compose Dialog 시리즈] DatePicker Dialog 구현하기 #4</title>
      <link>https://code-tales.tistory.com/30</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 이번 포스팅에서는 안드로이드 Jetpack Compose에서 DatePicker Dialog를 구현하는 방법에 대해 포스팅하겠습니다. 직접 구현할 수도 있겠지만, 지금은 compose material3에서 기본으로 제공해주는 DatePickerDialog와 DatePicker 2가지를 이용해서 DatePicker Dialog를 구현하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;1544&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXIcTG/btsFkHN9lIF/kdvPpTrKhZ8sAKaLU8DvLk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXIcTG/btsFkHN9lIF/kdvPpTrKhZ8sAKaLU8DvLk/img.jpg&quot; data-alt=&quot;안드로이드 compose material3을 이용해서 만든 date picker dialog&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXIcTG/btsFkHN9lIF/kdvPpTrKhZ8sAKaLU8DvLk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXIcTG%2FbtsFkHN9lIF%2FkdvPpTrKhZ8sAKaLU8DvLk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;안드로이드 compose material3을 이용해서 만든 date picker dialog&quot; loading=&quot;lazy&quot; width=&quot;238&quot; height=&quot;510&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;1544&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;안드로이드 compose material3을 이용해서 만든 date picker dialog&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. DatePicker Dialog 만들기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, compose material3에서 제공하는 DatePickerDialog와 DatePicker를 이용해서 UI를 만들겠습니다. &lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;datePickerState를 DatePicker에 전달해야하는데 UTC와 관련된 주의사항이 있습니다.&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;initialSelectedDateMillis에는 현재 선택된 날짜에 대한 밀리초 데이터를 전달해야하는데, 강제로 UTC 시간으로 적용됩니다. 즉, UTC를 고려하지않고 한국에서 밀리초로 변환된 날짜 데이터를 전달하면 -9시간이 적용된 결과가 DatePicker에 반영되기 때문에 원하는 결과가 나오지 않을 수 있습니다.&lt;br /&gt;&lt;br /&gt;그래서 반드시 Timezone을 세팅해주어야 합니다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// CustomDatePickerDialog.kt
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CustomDatePickerDialog(
    selectedDate: String?,
    onClickCancel: () -&amp;gt; Unit,
    onClickConfirm: (yyyyMMdd: String) -&amp;gt; Unit
) {
    DatePickerDialog(
        onDismissRequest = { onClickCancel() },
        confirmButton = {},
        colors = DatePickerDefaults.colors(
            containerColor = Color.White
        ),
        shape = RoundedCornerShape(6.dp)
    ) {
        val datePickerState = rememberDatePickerState(
            yearRange = 2023..2024,
            initialDisplayMode = DisplayMode.Picker,
            initialSelectedDateMillis = selectedDate?.let {
                val formatter = SimpleDateFormat(&quot;yyyyMMdd&quot;, Locale.getDefault()).apply {
                    // initialSelectedDateMillis는 UTC 시간을 받고 있다.
                    // 아래처럼 timeZone을 추가해서 UTC 시간으로 설정해야한다.
                    // 한국에서 실행한다면 -9시간을 적용하기 때문이다.
                    //
                    // 만약에 아래 코드가 없다면 20240228을 넘겼을 때, 안드로이드는 KST 20240228000000 으로 인식할 것이고,
                    // 이를 UTC 시간으로 변환하면서 -9시간을 적용하기 때문에 결과적으로 20240228을 넘기면 2024년 2월 27일에 선택이 되있는 문제가 발생한다.
                    timeZone = TimeZone.getTimeZone(&quot;UTC&quot;)
                }
                formatter.parse(it)?.time
                    ?: System.currentTimeMillis() // 날짜 파싱 실패 시 현재 시간을 기본값으로 사용
            } ?: System.currentTimeMillis(), // selectedDate가 null인 경우 현재 시간을 기본값으로 사용,
            selectableDates = object : SelectableDates {
                override fun isSelectableDate(utcTimeMillis: Long): Boolean {
                    // 날짜 제한 조건
                    return true
//                    return utcTimeMillis &amp;gt; System.currentTimeMillis()
                }
            })
        
        DatePicker(
            state = datePickerState,
        )
        
        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.Center,
        ) {
            Button(onClick = {
                onClickCancel()
            }) {
                Text(text = &quot;취소&quot;)
            }

            Spacer(modifier = Modifier.width(5.dp))

            Button(onClick = {
                datePickerState.selectedDateMillis?.let { selectedDateMillis -&amp;gt;
                    val yyyyMMdd = SimpleDateFormat(
                        &quot;yyyyMMdd&quot;,
                        Locale.getDefault()
                    ).format(Date(selectedDateMillis))

                    onClickConfirm(yyyyMMdd)
                }
            }) {
                Text(text = &quot;확인&quot;)
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. DatePicker Dialog 사용하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DatePickerDialog를 사용하기 위해서 CustomDatePickerDialogState를 만들었습니다. 이 클래스는 선택된 날짜, dialog 표시여부, 클릭 이벤트, 취소 이벤트에 대한 처리를 담당하고 있습니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// CustomDatePickerDialogState.kt
data class CustomDatePickerDialogState(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;var selectedDate: String? = null,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;var isShowDialog: Boolean = false,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;val onClickConfirm: (yyyyMMdd: String) -&amp;gt; Unit = {},
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;val onClickCancel: () -&amp;gt; Unit = {},
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;아래 코드는 ViewModel 입니다. 위에서 만들어준 클래스를 사용해서 상태관리를 해주고 있습니다. 사용자가 확인 버튼을 누른다면 isShowDialog를 false로 수정하고 selectedDate에 콜백함수로부터 전달받은 yyyyMMdd 형태의 값을 저장하고 있습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// MainViewModel.kt
@HiltViewModel
class MainViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle?,
) : ViewModel() {
    val customDatePickerDialogState: MutableState&amp;lt;CustomDatePickerDialogState?&amp;gt; =
        mutableStateOf(null)

    init {
        customDatePickerDialogState.value = CustomDatePickerDialogState(
            onClickConfirm = { yyyyMMdd -&amp;gt;
                customDatePickerDialogState.value = customDatePickerDialogState.value?.copy(
                    isShowDialog = false,
                    selectedDate = yyyyMMdd
                )
            },
            onClickCancel = {
                customDatePickerDialogState.value = customDatePickerDialogState.value?.copy(
                    isShowDialog = false
                )
            }
        )
    }

    fun showDatePickerDialog() {
        customDatePickerDialogState.value =
            customDatePickerDialogState.value?.copy(isShowDialog = true)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;아래는 UI를 보여주는 Screen 영역입니다. 버튼을 누르면 viewModel.showDatePickerDialog()를 호출해서 DatePickerDialog를 보여줍니다. 사용자가 날짜를 선택하고 확인 버튼을 누르면 dialog가 사라지고 Text 영역에 현재 선택한 날짜를 표시해줍니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// MainScreen.kt
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen(
    mainNavController: NavHostController,
    viewModel: MainViewModel
) {
    val customDatePickerDialogState = viewModel.customDatePickerDialogState.value

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text(text = &quot;메인화면&quot;) },
                navigationIcon = {
                    IconButton(onClick = { mainNavController.popBackStack() }) {
                        Icon(imageVector = Icons.Default.ArrowBack, contentDescription = null)
                    }
                }
            )
        }
    ) { paddingValues -&amp;gt;
        Column(
            modifier = Modifier.padding(paddingValues)
        ) {
           	Button(
                onClick = { viewModel.showDatePickerDialog() },
                shape = RectangleShape,
            ) {
                Text(
                    text = &quot;4. Show DatePickerDialog&quot;,
                    textAlign = TextAlign.Center,
                    style = TextStyle(
                        fontSize = 14.sp,
                        fontWeight = FontWeight.Bold
                    )
                )
            }
            Text(
                text = &quot;Selected Date: ${viewModel.customDatePickerDialogState.value?.selectedDate ?: &quot;날짜를 선택해주세요.&quot;}&quot;,
                style = TextStyle(
                    fontSize = 14.sp,
                    fontWeight = FontWeight.Bold
                )
            )

            if (customDatePickerDialogState?.isShowDialog == true) {
                CustomDatePickerDialog(
                    selectedDate = customDatePickerDialogState.selectedDate,
                    onClickCancel = customDatePickerDialogState.onClickCancel,
                    onClickConfirm = customDatePickerDialogState.onClickConfirm
                )
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. DatePicker Dialog 정리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #0d0d0d;&quot;&gt;안드로이드 J&lt;/span&gt;&lt;span style=&quot;color: #0d0d0d;&quot;&gt;etpack Compose Material3를 사용하여 DatePickerDialog를 구현하는 것은 사용자에게 효과적인 날짜 선택 인터페이스를 제공하는 좋은 방법&lt;/span&gt;&lt;span style=&quot;color: #0d0d0d;&quot;&gt;인 것 같습니다. UTC 시간대 처리와 같은 몇 가지 주의사항을&lt;/span&gt;&lt;span style=&quot;color: #0d0d0d;&quot;&gt; 고려해야 하지만, 위에서 설명한 내용을 이해했다면 문제 없이 구현할 수 있습니다.&lt;/span&gt;&lt;span style=&quot;color: #0d0d0d;&quot;&gt;&lt;br /&gt;&lt;br /&gt;이번 포스팅을 통해 안드로이드 Jetpack Compose Material3을 사용한 DatePickerDialog를 구현하는데 도움이 됐으면 좋겠습니다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>안드로이드/Compose Dialog</category>
      <category>Dialog</category>
      <category>Jetpack Compose</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/30</guid>
      <comments>https://code-tales.tistory.com/30#entry30comment</comments>
      <pubDate>Mon, 4 Mar 2024 11:30:22 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 Jetpack Compose Dialog 시리즈] Loading Dialog 구현하기 #3</title>
      <link>https://code-tales.tistory.com/29</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 이번 포스팅에서는 안드로이드 Jetpack Compose에서 전역으로 사용할 수 있는 로딩 팝업창을 띄우는 방법에 대해 포스팅하겠습니다. 로딩을 보여주는 방법은 무수히 많지만, 저는 xml 시절부터 안드로이드에서 제공하는 기본 dialog를 상속받아서 구현하는 형태로 많이 사용했습니다. &lt;br&gt;&lt;br&gt;왜냐하면 dialog에는 dismissOnBackPress, dismissOnClickOutSide 등 기본으로 제공하는 기능이 많아서 개발자가 따로 구현하지 않아도 되기 때문입니다. Compose에서도 비슷한 방법으로 간단하게 로딩창을 보여줄 수 있습니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;1544&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cUfHA4/btsFiNUIaAW/zUf3GkIJCkdzda3DlesSq0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cUfHA4/btsFiNUIaAW/zUf3GkIJCkdzda3DlesSq0/img.jpg&quot; data-alt=&quot;전역 로딩 팝업창&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cUfHA4/btsFiNUIaAW/zUf3GkIJCkdzda3DlesSq0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcUfHA4%2FbtsFiNUIaAW%2FzUf3GkIJCkdzda3DlesSq0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;전역 로딩 팝업창&quot; loading=&quot;lazy&quot; width=&quot;279&quot; height=&quot;598&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;1544&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;전역 로딩 팝업창&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. Loading Dialog 만들기&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;전역 로딩 팝업창을 만들기 위해 먼저 LoadingState라는 싱글턴 객체를 만들어 로딩 상태를 관리합니다. 그런 다음, Compose의 Dialog 컴포저블을 사용하여 GlobalLoadingScreen을 구현합니다.&lt;/p&gt;&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt; 
 &lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// CustomLoadingDialogState.kt
object LoadingState {
    private val _isLoading = MutableStateFlow(false) // 초기값을 false로 설정
    val isLoading: StateFlow&amp;lt;Boolean&amp;gt; = _isLoading.asStateFlow()

    fun show() {
        _isLoading.value = true
    }

    fun hide() {
        _isLoading.value = false
    }
}&lt;/code&gt;&lt;/pre&gt; 
&lt;/div&gt;&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt; 
 &lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// CustomLoadingDialog.kt
@Composable
fun GlobalLoadingScreen() {
    val isLoading = LoadingState.isLoading.collectAsState().value

    if (isLoading) {
        Dialog(
            onDismissRequest = { LoadingState.hide() },
            properties = DialogProperties(
                dismissOnBackPress = true,
                dismissOnClickOutside = true,
            )
        ) {
            CircularProgressIndicator()
        }
    }
}&lt;/code&gt;&lt;/pre&gt; 
&lt;/div&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. Loading Dialog 사용하기&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;로딩 팝업창을 전역에서 사용하기 위해서는 앱의 최상위 Composable 함수에 GlobalLoadingScreen을 추가해야 합니다. 이렇게 구현하면 어느 곳에서든 LoadingState.show()와 LoadingState.hide()를 호출하여 로딩 화면을 표시하거나 숨길 수 있습니다. 예를 들어, MainActivity의 setContent 내에 GlobalLoadingScreen을 추가합니다:&lt;/p&gt;&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt; 
 &lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// MainActivity.kt
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            val navController = rememberNavController()
            CleanarchitectureexampleTheme {
                Box {
                    MainGraph(mainNavController = navController)
                    GlobalLoadingScreen()
                }
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt; 
&lt;/div&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 아래와 같은 형태로 LoadingState.show()를 호출하면 로딩 팝업창이 나오게 됩니다.&lt;/p&gt;&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt; 
 &lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;Button(
    onClick = { LoadingState.show() },
    shape = RectangleShape,
) {
    Text(
        text = &quot;3. Show LoadingDialog&quot;,
        textAlign = TextAlign.Center,
        style = TextStyle(
            fontSize = 14.sp,
            fontWeight = FontWeight.Bold
        )
    )
}&lt;/code&gt;&lt;/pre&gt; 
&lt;/div&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. Loading Dialog 정리&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이&amp;nbsp;구현&amp;nbsp;방법의&amp;nbsp;장점은&amp;nbsp;로딩&amp;nbsp;상태의&amp;nbsp;관리와&amp;nbsp;UI&amp;nbsp;표시를&amp;nbsp;분리한다는&amp;nbsp;것입니다.&amp;nbsp;LoadingState&amp;nbsp;객체를&amp;nbsp;사용하여&amp;nbsp;애플리케이션의&amp;nbsp;어느&amp;nbsp;곳에서나&amp;nbsp;로딩&amp;nbsp;상태를&amp;nbsp;제어할&amp;nbsp;수&amp;nbsp;있으며,&amp;nbsp;GlobalLoadingScreen은&amp;nbsp;이&amp;nbsp;상태에&amp;nbsp;반응하여&amp;nbsp;사용자에게&amp;nbsp;로딩&amp;nbsp;인디케이터를&amp;nbsp;보여줍니다.&lt;br&gt;&lt;br&gt;또한, Dialog를 통해 구현하면 DialogProperties 등의 속성을 통해서 뒤로 가기 버튼이나 바깥쪽 클릭으로 다이얼로그를 닫을 수 있는 옵션을 기본으로 사용할 수 있습니다. 개발자가 직접 신경써야하는 부분도 기본 Dialog에 있기 때문에 편리하고, 필요에 따라 이러한 속성을 조정하여 로딩 다이얼로그의 동작을 수정할 수 있습니다.&lt;br&gt;&lt;br&gt;이번 포스팅에서는 간단하게 전역 로딩 팝업창을 구현해봤는데, 해당 내용을 참고해서 원하는 형태의 로딩 팝업창을 구현하는데 도움이 됐으면 좋겠습니다.&lt;/p&gt;</description>
      <category>안드로이드/Compose Dialog</category>
      <category>Dialog</category>
      <category>Jetpack Compose</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/29</guid>
      <comments>https://code-tales.tistory.com/29#entry29comment</comments>
      <pubDate>Sun, 3 Mar 2024 11:30:46 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 Jetpack Compose Dialog 시리즈] BottomSheetDialog 구현하기 #2</title>
      <link>https://code-tales.tistory.com/28</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 이번 포스팅에서는 안드로이드 Jetpack Compose에서 BottomSheetDialog를 구현하는 방법에 대해서 설명하겠습니다. BottomSheetDialog는 하단에서 위로 올라오는 형태의 dialog로써 현대 앱에서 많이 볼 수 있습니다. 기존의 xml에서 구현할 때보다 많이 쉬워졌는데 아래 예제를 통해서 설명하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;modal_bottom_sheet.jpg&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;1544&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buj54K/btsFgN9adEC/OyAWmFYOio90NlT0KB2Cj0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buj54K/btsFgN9adEC/OyAWmFYOio90NlT0KB2Cj0/img.jpg&quot; data-alt=&quot;안드로이드 Jetpack Compose에서 구현한 BottomSheetDialog&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buj54K/btsFgN9adEC/OyAWmFYOio90NlT0KB2Cj0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbuj54K%2FbtsFgN9adEC%2FOyAWmFYOio90NlT0KB2Cj0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;안드로이드 Jetpack Compose에서 구현한 BottomSheetDialog&quot; loading=&quot;lazy&quot; width=&quot;283&quot; height=&quot;607&quot; data-filename=&quot;modal_bottom_sheet.jpg&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;1544&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;안드로이드 Jetpack Compose에서 구현한 BottomSheetDialog&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. Compose에서 BottomSheetDialog 사용하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 Jetpack Compose에서 BottomSheetDialog를 사용하면 사용자에게 추가 정보를 제공하거나 선택을 요구하는 등의 상호작용을 제공하는 상황에서 효과적입니다. Compose에서는 Material3에서 제공하는 ModalBottomSheet 컴포저블을 활용하여 손쉽게 구현할 수 있습니다.&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. BottomSheetDialog 만들기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: left;&quot;&gt;먼저, Mod&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: left;&quot;&gt;alBottomSheet 컴포저블을 활용하여 BottomSheetDialog의 레이아웃을 만듭니다. ModalBott&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: left;&quot;&gt;omSheet은 하단에서 슬라이드 업되어 나타날 UI(컴포저블)를 정의할 수 있고, sheetState를 사용하면 BottomSheet의 상태(확장, 숨김)를 제어할 수도 있습니다. 아래 코드는 BottomSheetDialog의 높이값을 300dp로 고정하고 Text, Button, Column(ScrollView)를 구현한 예제입니다.&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// CustomBottomSheetDialog.kt
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CustomBottomSheetDialog(
    title: String,
    description: String,
    onClickCancel: () -&amp;gt; Unit,
    onClickConfirm: () -&amp;gt; Unit
) {
    val modalBottomSheetState = rememberModalBottomSheetState()
    val scope = rememberCoroutineScope()
    val bottomPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()

    ModalBottomSheet(
        onDismissRequest = { onClickCancel() },
        sheetState = modalBottomSheetState,
        dragHandle = { BottomSheetDefaults.DragHandle() },
    ) {
        Column(
            modifier = Modifier
                .padding(top = 10.dp, start = 10.dp, end = 10.dp, bottom = bottomPadding)
                .fillMaxWidth()
                .height(300.dp),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text(
                text = title,
                textAlign = TextAlign.Center,
                style = TextStyle(
                    color = Color.Black,
                    fontSize = 16.sp,
                    fontWeight = FontWeight.Bold
                )
            )
            Spacer(modifier = Modifier.height(10.dp))

            Text(
                text = description,
                textAlign = TextAlign.Center,
                style = TextStyle(
                    color = Color.Gray,
                    fontSize = 14.sp,
                    fontWeight = FontWeight.Normal
                )
            )

            Spacer(modifier = Modifier.height(10.dp))

            // 고정 높이 200.dp의 컨테이너 내 스크롤 가능한 국기 이모지 표시
            Column(
                modifier = Modifier
                    .fillMaxWidth()
                    .height(100.dp)
                    .padding(10.dp)
                    .verticalScroll(rememberScrollState()),
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                CountryList()
            }

            Spacer(modifier = Modifier.height(10.dp))

            Button(onClick = {
                scope.launch {
                    modalBottomSheetState.hide()
                }.invokeOnCompletion {
                    onClickConfirm()
                }
            }) {
                Text(&quot;Confirm&quot;)
            }
        }
    }
}

@Composable
fun CountryList() {
    val countries = listOf(
        Pair(&quot;United States&quot;, &quot;\uD83C\uDDFA\uD83C\uDDF8&quot;),
        Pair(&quot;Canada&quot;, &quot;\uD83C\uDDE8\uD83C\uDDE6&quot;),
        Pair(&quot;India&quot;, &quot;\uD83C\uDDEE\uD83C\uDDF3&quot;),
        Pair(&quot;Germany&quot;, &quot;\uD83C\uDDE9\uD83C\uDDEA&quot;),
        Pair(&quot;France&quot;, &quot;\uD83C\uDDEB\uD83C\uDDF7&quot;),
        Pair(&quot;Japan&quot;, &quot;\uD83C\uDDEF\uD83C\uDDF5&quot;),
        Pair(&quot;China&quot;, &quot;\uD83C\uDDE8\uD83C\uDDF3&quot;),
        Pair(&quot;Brazil&quot;, &quot;\uD83C\uDDE7\uD83C\uDDF7&quot;),
        Pair(&quot;Australia&quot;, &quot;\uD83C\uDDE6\uD83C\uDDFA&quot;),
        Pair(&quot;Russia&quot;, &quot;\uD83C\uDDF7\uD83C\uDDFA&quot;),
        Pair(&quot;United Kingdom&quot;, &quot;\uD83C\uDDEC\uD83C\uDDE7&quot;),
    )
    Column {
        countries.forEach {
            Text(text = it.second)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. BottomSheetDialog 사용하&lt;/b&gt;&lt;b&gt;기&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;안드로이드&amp;nbsp;Jetpack Compose의 BottomSheetDialog를 사용하기 위해서는 상태 관리가 필요합니다. 해당 dialog를 공통으로 사용하기 위해서 CustomBottomSheetDialogState 타입을 만들어주었고, MutableState를 통해 표시 여부를 제어했습니다. dialog의 노출여부를 title값이 공백인지 아닌지로 판단했는데, 좀 더 명확한 boolean값을 사용하면 더 좋을 것 같습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// CustomBottomSheetDialogState.ke
data class CustomBottomSheetDialogState(
    val title: String = &quot;&quot;,
    val description: String = &quot;&quot;,
    val onClickConfirm: () -&amp;gt; Unit = {},
    val onClickCancel: () -&amp;gt; Unit = {},
)

// MainViewModel.kt
@HiltViewModel
class MainViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle?,
) : ViewModel() {
    val customBottomSheetDialogState: MutableState&amp;lt;CustomBottomSheetDialogState&amp;gt; = mutableStateOf(
        CustomBottomSheetDialogState()
    )

    fun showBottomSheetDialog() {
        customBottomSheetDialogState.value = CustomBottomSheetDialogState(
            title = &quot;국기 이모지&quot;,
            description = &quot;나라별 국기 이모지를 확인해보세요.&quot;,
            onClickConfirm = {
                resetBottomSheetDialogState()
            },
            onClickCancel = {
                resetBottomSheetDialogState()
            }
        )
    }

    fun resetBottomSheetDialogState() {
        customBottomSheetDialogState.value = CustomBottomSheetDialogState()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// MainScreen.kt
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen(
    mainNavController: NavHostController,
    viewModel: MainViewModel
) {

    val customBottomSheetDialogState = viewModel.customBottomSheetDialogState.value

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text(text = &quot;메인화면&quot;) },
                navigationIcon = {
                    IconButton(onClick = { mainNavController.popBackStack() }) {
                        Icon(imageVector = Icons.Default.ArrowBack, contentDescription = null)
                    }
                }
            )
        }
    ) { paddingValues -&amp;gt;
        Column(
            modifier = Modifier.padding(paddingValues)
        ) {
        
        	...
            
            Button(
                onClick = { viewModel.showBottomSheetDialog() },
                shape = RectangleShape,
            ) {
                Text(
                    text = &quot;2. Show BottomSheetDialog&quot;,
                    textAlign = TextAlign.Center,
                    style = TextStyle(
                        fontSize = 14.sp,
                        fontWeight = FontWeight.Bold
                    )
                )
            }

            if (customBottomSheetDialogState.title.isNotBlank()) {
                CustomBottomSheetDialog(
                    title = customBottomSheetDialogState.title,
                    description = customBottomSheetDialogState.description,
                    onClickCancel = { customBottomSheetDialogState.onClickCancel() },
                    onClickConfirm = { customBottomSheetDialogState.onClickConfirm() }
                )
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. BottomSheetDialog 정리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 안드로이드 Jetpack Compose의&amp;nbsp; ModalBottomSheet를 사용해서 BottomSheetDialog를 구현했습니다. Compose에서 제공하는 ModalBottomSheetState를 활용하면 BottomSheetDialog의 상태를 확인할 수 있고, show(), hide() 등의 함수를 호출해서 dialog의 출현을 제어할 수도 있습니다.&lt;br /&gt;&lt;br /&gt;위에 작성한 예제처럼 컴포저블을 자유롭게 배치해서 원하는 형태의 BottomSheetDialog를 구현하는데 도움이 됐으면 좋겠습니다.&lt;/p&gt;</description>
      <category>안드로이드/Compose Dialog</category>
      <category>Dialog</category>
      <category>Jetpack Compose</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/28</guid>
      <comments>https://code-tales.tistory.com/28#entry28comment</comments>
      <pubDate>Sat, 2 Mar 2024 11:30:56 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 Jetpack Compose Dialog 시리즈] 커스텀 Dialog 구현하기 #1</title>
      <link>https://code-tales.tistory.com/27</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 이번 포스팅에서는 안드로이드 Jetpack Compose에서 Custom Dialog를 구현하는 방법에 대해서 설명하겠습니다. &lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;Co&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;mpose는 선언적 UI 프레임&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;워크로, UI를 구성하는 방식이 기존의 XML 기반 방식과는 크게 다릅니다. 이러한 차이로 인해 Dialo&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;g를 구현하는 방식에도 몇 가지 변화가 생겼습니다.&lt;/span&gt; &lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;custom_dialog.jpg&quot; data-origin-width=&quot;432&quot; data-origin-height=&quot;926&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpOLXi/btsFf4bu9Ga/QEx1fDqg209GbQC150nnY1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpOLXi/btsFf4bu9Ga/QEx1fDqg209GbQC150nnY1/img.jpg&quot; data-alt=&quot;안드로이드 Jetpack Compose에서 구현한 커스텀 dialog&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpOLXi/btsFf4bu9Ga/QEx1fDqg209GbQC150nnY1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcpOLXi%2FbtsFf4bu9Ga%2FQEx1fDqg209GbQC150nnY1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;안드로이드 Jetpack Compose에서 구현한 커스텀 dialog&quot; loading=&quot;lazy&quot; width=&quot;261&quot; height=&quot;559&quot; data-filename=&quot;custom_dialog.jpg&quot; data-origin-width=&quot;432&quot; data-origin-height=&quot;926&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;안드로이드 Jetpack Compose에서 구현한 커스텀 dialog&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. Compose에서 Dialog 구현하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jetpack Compose에서 Dialog를 사용하는 기본적인 방법은 Dialog 컴포저블을 직접 사용하는 것입니다. Dialog 컴포저블을 사용하면, Dialog의 기본적인 요소들이 전부 구현돼있는 것을 사용하면 됩니다. 그리고 Compose의 상태 관리 시스템과도 잘 통합되어 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. Custom Dialog 만들기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compose에서&amp;nbsp;커스텀&amp;nbsp;Dialog를&amp;nbsp;만들기&amp;nbsp;위해서는&amp;nbsp;Dialog&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;Compose&amp;nbsp;UI&amp;nbsp;요소를&amp;nbsp;포함할&amp;nbsp;수&amp;nbsp;있습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// CustomAlertDialog.kt
@Composable
fun CustomAlertDialog(
    title: String,
    description: String,
    onClickCancel: () -&amp;gt; Unit,
    onClickConfirm: () -&amp;gt; Unit
) {
    Dialog(
        onDismissRequest = { onClickCancel() },
        properties = DialogProperties(
            dismissOnBackPress = true,
            dismissOnClickOutside = true,
        )
    ) {
        Card(
            shape = RoundedCornerShape(8.dp), // Card의 모든 꼭지점에 8.dp의 둥근 모서리 적용
        )
        {
            Column(
                modifier = Modifier
                    .width(300.dp)
                    .wrapContentHeight()
                    .background(
                        color = Color.White,
                    ),
                horizontalAlignment = Alignment.CenterHorizontally,
            ) {

                Spacer(modifier = Modifier.height(40.dp))

                Text(
                    text = title,
                    textAlign = TextAlign.Center,
                    style = TextStyle(
                        color = Color.Black,
                        fontSize = 16.sp,
                        fontWeight = FontWeight.Bold
                    )
                )

                Spacer(modifier = Modifier.height(10.dp))

                Text(
                    text = description,
                    textAlign = TextAlign.Center,
                    style = TextStyle(
                        color = Color.LightGray,
                        fontSize = 14.sp,
                        fontWeight = FontWeight.Normal
                    )
                )

                Spacer(modifier = Modifier.height(40.dp))

                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(1.dp)
                        .background(color = Color.LightGray)
                )
                Row(
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(IntrinsicSize.Min) // Row의 높이를 내부 컴포넌트에 맞춤
                ) {
                    Button(
                        onClick = { onClickCancel() },
                        shape = RectangleShape,
                        modifier = Modifier
                            .weight(1f)
                            .fillMaxHeight(),
                        colors = ButtonDefaults.buttonColors(
                            containerColor = Color.White, // 버튼 배경색상
                            contentColor = Color.Black, // 버튼 텍스트 색상
                            disabledContainerColor = Color.Gray, // 버튼 비활성화 배경 색상
                            disabledContentColor = Color.White, // 버튼 비활성화 텍스트 색상
                        ),

                        ) {
                        Text(
                            text = &quot;취소&quot;,
                            textAlign = TextAlign.Center,
                            style = TextStyle(
                                fontSize = 14.sp,
                                fontWeight = FontWeight.Normal
                            )
                        )
                    }

                    Box(
                        modifier = Modifier
                            .fillMaxHeight()
                            .width(1.dp)
                            .background(color = Color.LightGray)
                    )

                    Button(
                        onClick = { onClickConfirm() },
                        shape = RectangleShape,
                        modifier = Modifier
                            .weight(1f)
                            .fillMaxHeight(),
                        colors = ButtonDefaults.buttonColors(
                            containerColor = Color.White, // 버튼 배경색상
                            contentColor = Color.Red, // 버튼 텍스트 색상
                            disabledContainerColor = Color.Gray, // 버튼 비활성화 배경 색상
                            disabledContentColor = Color.White, // 버튼 비활성화 텍스트 색상
                        ),
                    ) {
                        Text(
                            text = &quot;삭제&quot;,
                            textAlign = TextAlign.Center,
                            style = TextStyle(
                                fontSize = 14.sp,
                                fontWeight = FontWeight.Bold
                            )
                        )
                    }
                }
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. Custom Dialog 사용하&lt;/b&gt;&lt;b&gt;기&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;안드로이드 Jetpack Compose의 커스텀 Dialog를 사용하기 위해서는 상태 관리가 필요합니다. 여기서는 MutableState&amp;lt;CustomAlertDialogState&amp;gt; 타입의 상태를 통해 Dialog의 표시 여부를 제어했습니다. 이 상태는 ViewModel이나 Composable 함수 내부에서 관리될 수 있는데, ViewModel에서 제어하도록 구현했습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// CustomAlertDialogState.kt
data class CustomAlertDialogState(
    val title: String = &quot;&quot;,
    val description: String = &quot;&quot;,
    val onClickConfirm: () -&amp;gt; Unit = {},
    val onClickCancel: () -&amp;gt; Unit = {},
)

// MainViewModel.kt
@HiltViewModel
class MainViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle?,
) : ViewModel() {
    val customAlertDialogState: MutableState&amp;lt;CustomAlertDialogState&amp;gt; = mutableStateOf&amp;lt;CustomAlertDialogState&amp;gt;(
        CustomAlertDialogState()
    )
    fun showCustomAlertDialog() {
        customAlertDialogState.value = CustomAlertDialogState(
            title = &quot;정말로 삭제하시겠습니까?&quot;,
            description = &quot;삭제하면 복구할 수 없습니다.&quot;,
            onClickConfirm = {
                resetDialogState()
            },
            onClickCancel = {
                resetDialogState()
            }
        )
    }
    // 다이얼로그 상태 초기화
    fun resetDialogState() {
        customAlertDialogState.value = CustomAlertDialogState()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// MainScreen.kt
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen(
    mainNavController: NavHostController,
    viewModel: MainViewModel
) {

    val customAlertDialogState = viewModel.customAlertDialogState.value

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text(text = &quot;메인화면&quot;) },
                navigationIcon = {
                    IconButton(onClick = { mainNavController.popBackStack() }) {
                        Icon(imageVector = Icons.Default.ArrowBack, contentDescription = null)
                    }
                }
            )
        }
    ) { paddingValues -&amp;gt;
        Column(
            modifier = Modifier.padding(paddingValues)
        ) {
            Button(
                onClick = { viewModel.showCustomAlertDialog() },
                shape = RectangleShape,
            ) {
                Text(
                    text = &quot;1. Show AlertDialog&quot;,
                    textAlign = TextAlign.Center,
                    style = TextStyle(
                        fontSize = 14.sp,
                        fontWeight = FontWeight.Bold
                    )
                )
            }

            if (customAlertDialogState.title.isNotBlank()) {
                CustomAlertDialog(
                    title = customAlertDialogState.title,
                    description = customAlertDialogState.description,
                    onClickCancel = { customAlertDialogState.onClickCancel() },
                    onClickConfirm = { customAlertDialogState.onClickConfirm() }
                )
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. Custom Dialog 정리&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;안드로이드 Jetpack Compose를 사용하여 커스텀 Dialog를 구현하는 방법을 살펴보았습니다. 기존의 XML 기반의 UI 개발과는 다른 접근 방식을 제공하지만, Compose의 선언적인 특성 덕분에 UI 상태 관리가 보다 간결하고 직관적으로 이루어집니다. Compose의 Dialog 컴포저블을&amp;nbsp;사용하여&amp;nbsp;다양한&amp;nbsp;커스텀&amp;nbsp;Dialog를&amp;nbsp;쉽게&amp;nbsp;구현할&amp;nbsp;수&amp;nbsp;있습니다.&lt;br /&gt;&lt;br /&gt;다음 포스팅에서는 Jetpack Compose에서 BottomSheetDialog를 구현하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>안드로이드/Compose Dialog</category>
      <category>Dialog</category>
      <category>Jetpack Compose</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/27</guid>
      <comments>https://code-tales.tistory.com/27#entry27comment</comments>
      <pubDate>Fri, 1 Mar 2024 11:30:24 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 Jetpack Compose Paging3 시리즈] RemoteMediator 적용하기 #11</title>
      <link>https://code-tales.tistory.com/26</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. RemoteMediator를 Pager에 적용하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 지난 포스팅에서는 안드로이드 Jetpack Compose의 Paging3 라이브러리에서 제공하는 RemoteMediator를 구현하고, 코드를 분석하면서 어떤 역할을 하는지에 대해 살펴보았습니다. 이번 포스팅은 RemoteMediator와 PageSource를 Pager에 전달해서 화면에 정상적으로 UI가 보이는지 확인하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;movie_list_cache.jpg&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;1544&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOL1qL/btsFhiz2HCr/k44bP3n8iDZnL5zGKWjVVK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOL1qL/btsFhiz2HCr/k44bP3n8iDZnL5zGKWjVVK/img.jpg&quot; data-alt=&quot;비행기 모드(데이터 사용 X)를 키고 앱을 실행시켰을 때, 캐시 데이터를 사용해서 영화 목록을 보여주는 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOL1qL/btsFhiz2HCr/k44bP3n8iDZnL5zGKWjVVK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOL1qL%2FbtsFhiz2HCr%2Fk44bP3n8iDZnL5zGKWjVVK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;캐시 데이터로 영화목록을 불러온 모습&quot; loading=&quot;lazy&quot; width=&quot;307&quot; height=&quot;658&quot; data-filename=&quot;movie_list_cache.jpg&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;1544&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;비행기 모드(데이터 사용 X)를 키고 앱을 실행시켰을 때, 캐시 데이터를 사용해서 영화 목록을 보여주는 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 안드로이드 Jetpack Compose Pager 구현 과정&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pager의 매개변수에 RemoteMediator와 PagingSource를 전달해주어야 합니다. Hilt 라이브러리를 이용해서 의존성을 주입받고, Pager 객체에 적절한 매개변수를 전달해서 RemoteMediator 기능이 적용된 Pager를 반환하는 함수를 MovieRepository 내부에 구현하겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-1. RemoteMediator 의존성 주입하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MovieRepository에서 MovieRemoteMedaitor를 사용하기 위해서 의존성을 주입하겠습니다. RemoteMediator에서는 네트워크 통신과 로컬 데이터베이스를 모두 해야 하기 때문에 RemoteDataSource와 LocalDataSource를 모두 주입받는 모습입니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// DataModule.kt

@Provides
@Singleton
fun provideMovieRepository(
    movieRemote: MovieDataSource.Remote,
    movieLocal: MovieDataSource.Local,
    movieRemoteMediator: MovieRemoteMediator
): MovieRepository {
    return MovieRepositoryImpl(
        remote = movieRemote,
        local = movieLocal,
        remoteMediator = movieRemoteMediator
    )
}

@Provides
@Singleton
fun provideMovieMediator(
    movieLocalDataSource: MovieDataSource.Local,
    movieRemoteDataSource: MovieDataSource.Remote
): MovieRemoteMediator {
    return MovieRemoteMediator(movieLocalDataSource, movieRemoteDataSource)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-2. MovieRepository에서 Paging 데이터 스트림 반환하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;네트워크 통신으로만 영화 목록을 가져오는 movies 함수 및 로컬 데이터베이스와 네트워크 통신 모두 사용하는 moviesUsingRemoteMediator 함수의 모습입니다. PagingSource를 제공하는 부분과 RemoteMediator의 존재에서 차이가 생깁니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// MovieRepositoryImpl.kt (Data Layer)

class MovieRepositoryImpl(
    private val remote: MovieDataSource.Remote,
    private val local: MovieDataSource.Local,
    private val remoteMediator: MovieRemoteMediator
) : MovieRepository {
    /**
     * 네트워크 통신만을 사용하여 영화 목록 가져오기
     */
    override fun movies(pageSize: Int): Flow&amp;lt;PagingData&amp;lt;MovieEntity&amp;gt;&amp;gt; {
        return Pager(
            config = PagingConfig(
                pageSize = pageSize,
                enablePlaceholders = false,
            ),
            pagingSourceFactory = { MoviesPagingSource(remote) }
        ).flow
    }

    /**
     * 로컬 데이터베이스와 네트워크 통신을 사용하여 영화 목록 가져오기
     */
    @OptIn(ExperimentalPagingApi::class)
    override fun moviesUsingRemoteMediator(pageSize: Int): Flow&amp;lt;PagingData&amp;lt;MovieEntity&amp;gt;&amp;gt; {
        return Pager(
            config = PagingConfig(
                pageSize = pageSize,
                enablePlaceholders = false,
            ),
            pagingSourceFactory = { local.pagingSource() },
            remoteMediator = remoteMediator
        ).flow.map { pagingData -&amp;gt;
            pagingData.map { it.toDomain() }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-3. Domain Layer에서 moviesUsingRemoteMediator를 호출하도록 변경&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;기존에는 Domain Layer에서 movies함수를 호출하고 있었습니다. RemoteMediator를 적용한 moviesUsingRemoteMediator를 호출하도록 변경하면 됩니다.&lt;br /&gt;&lt;br /&gt;UI Layer에서는 별도의 수정 없이 정상적으로 작동하는 모습을 확인할 수 있습니다.&lt;br /&gt;이렇게 클린 아키텍처의 장점도 확인할 수 있습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// Getmovies.kt (Domain Layer)
class GetMovies (
    private val movieRepository: MovieRepository
){
//    operator fun invoke(pageSize: Int): Flow&amp;lt;PagingData&amp;lt;MovieEntity&amp;gt;&amp;gt; = movieRepository.movies(pageSize)
    operator fun invoke(pageSize: Int): Flow&amp;lt;PagingData&amp;lt;MovieEntity&amp;gt;&amp;gt; = movieRepository.moviesUsingRemoteMediator(pageSize)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 안드로이드 Jetpack Compose Paging3 시리즈를 마치며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: left;&quot;&gt;이번 시리즈를 통해 안드로이드 Jetpack Compose의 Paging3 라이브러리 사용 방법과 RemoteMediator를 통한 효율적인 데이터 관리 방법을 알아보았습니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;RemoteMediator를 적용하면 네트워크 데이터와 로컬 데이터베이스를 결합하여 사용자에게 더 빠른 데이터 로드 및 좋은 UX를 제공할 수 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;그러나 RemoteMediator의 구현이 필수는 아니기 때문에 진행하는 프로젝트의 성격에 맞춰서 판단하면 좋을 것 같습니다.&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>안드로이드/Compose Paging3</category>
      <category>Jetpack Compose</category>
      <category>pager</category>
      <category>paging3</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/26</guid>
      <comments>https://code-tales.tistory.com/26#entry26comment</comments>
      <pubDate>Thu, 29 Feb 2024 11:30:10 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 Jetpack Compose Paging3 시리즈] RemoteMediator 코드 설명 #10</title>
      <link>https://code-tales.tistory.com/25</link>
      <description>&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. RemoteMediator 코드 분석을 하는 이유&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 지난 포스팅에서는 안드로이드 Jetpack Compose Paging3 라이브러리를 사용하기 위해서 LocalDataSource와 RemoteMediator를 구현했습니다. 해당 라이브러리의 정확한 원리를 이해해야 응용력도 생기고 개발자로서의 성장에도 도움이 되기 때문에 이번 포스팅에서는 RemoteMediator의 코드를 분석하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1559&quot; data-origin-height=&quot;1732&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcz1g7/btsFg2RsTIg/ys0aqp2n8SxU9NPSer8Rk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcz1g7/btsFg2RsTIg/ys0aqp2n8SxU9NPSer8Rk0/img.png&quot; data-alt=&quot;RemoteMediator를 상속받아서 구현한 MovieRemoteMediator&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcz1g7/btsFg2RsTIg/ys0aqp2n8SxU9NPSer8Rk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbcz1g7%2FbtsFg2RsTIg%2Fys0aqp2n8SxU9NPSer8Rk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;RemoteMediator를 상속받아서 구현한 MovieRemoteMediator 코드&quot; loading=&quot;lazy&quot; width=&quot;1559&quot; height=&quot;1732&quot; data-origin-width=&quot;1559&quot; data-origin-height=&quot;1732&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;RemoteMediator를 상속받아서 구현한 MovieRemoteMediator&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. RemoteMediator의 LoadType, MediatorResult 정리&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;안드로이드 Jetpack Compose Paging3의 RemoteMediator를 사용하려면 RemoteMediator라는 추상클래스를 상속받아서 구현하게 됩니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;해당 클래스에서는 LoadType, PagingState를 제공해 주고 MediatorResult를 반환해야만 하는데 각각의 기능과 역할에 대해서 알아보겠습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-1. RemoteMediator LoadType의 세 가지 상태&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LoadType은 스크롤을 할 때 페이지가 어떤 상태인지 알려주는 역할을 합니다. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;LoadType에서는 세 가지 상태가 있는데 각각의 상태에 따른 적절한 처리를 해줘야 합니다.&amp;nbsp;&lt;/span&gt;사용자가 맨 위로 스크롤을 한다면 LoadType은 PREPEND가 될 것이고, 맨 아래로 스크롤을 했다면 APPEND가 되는 형태입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2-1-1. LoadType.REFRESH&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자에 의한 새로고침 또는 PagingSource의 무효화가 발생했을 때 LoadType.REFRESH가 전달됩니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;이 상태에서는 페이지를 다시 로드해야 하므로 페이지의 첫 번째를 뜻하는 '1' 값을 반환했습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2-1-2. LoadType.PREPEND&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;첫 번째 페이지 데이터로 스크롤했을 때 LoadType.PREPEND가 전달됩니다. 현재 로드된 데이터의 앞쪽에 데이터를 추가하는 작업을 나타냅니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;이번 프로젝트에서 구현하는 페이징 시스템은 아래에만 데이터가 붙는 형태이기 때문에 MediatorResult.Success(endOfPaginationReached=true)를 반환해서 별다른 작업을 하지 않아도 됩니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2-1-3. LoadType.APPEND&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;마지막 페이지 데이터로 스크롤했을 때 LoadType.APPEND가 전달됩니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;현재 로드된 데이터의 뒤쪽에 데이터를 추가하는 작업을 나타냅니다. 다음 페이지가 있으면 다음 페이지 값을 반환하고, 없으면 MediatorResult.Success(endOfPaginationReached=true)를 반환해서 작업을 종료합니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-2. PagingState에 저장된 정보&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PagingState는&amp;nbsp;RemoteMediator의 load 함수에 전달되는 매개변수 중 하나로, 현재 Paging 상태에 대한 정보를 포함합니다.&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2-2-1. anchorPosition&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가&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;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2-2-2. closestPageToPosition &lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;주어진&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;PREPEND&amp;nbsp;또는&amp;nbsp;APPEND&amp;nbsp;작업&amp;nbsp;시&amp;nbsp;다음에&amp;nbsp;로드할&amp;nbsp;페이지&amp;nbsp;번호를&amp;nbsp;결정하는&amp;nbsp;데&amp;nbsp;유용합니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2-2-3. config &lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Pager 객체게 전달한 PagingConfig 설정에 대한 정보를 제공합니다. 페이지 크기(pageSize), 프리페치 거리(prefetchDistance), 초기 로드 크기(initialLoadSize) 등이 포함되고, 네트워크 요청 시 요청할 데이터의 양을 해당 정보를 이용해서 설정할 수 있습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-3. MediatorResult의 두 가지 결과&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RemoteMediator를 구현할 때 데이터 로드의 성공, 실패 여부에 대해 반환해주어야 합니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;MediatorResult는 Sealed class로 Success와 Error라는 2가지 상태가 있는데 적절하게 반환하면 됩니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2-3-1. MediatorResult.Success&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 로드 작업이 성공적으로 완료되었음을 나타냅니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;endOfPaginationReached 속성을 통해 더 이상 로드할 데이터가 없음을 알릴 수 있습니다. 모든 데이터를 로드했거나, 페이징의 끝에 도달했을 때 true로 전달해서 MediatorResult.Success를 반환하면 됩니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2-3-2. MediatorResult.Error&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터&amp;nbsp;로드&amp;nbsp;작업&amp;nbsp;중&amp;nbsp;오류가&amp;nbsp;발생했음을&amp;nbsp;나타냅니다.&amp;nbsp;이&amp;nbsp;경우,&amp;nbsp;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;예외&amp;nbsp;객체를&amp;nbsp;전달하여&amp;nbsp;어떤&amp;nbsp;오류가&amp;nbsp;발생했는지&amp;nbsp;알릴&amp;nbsp;수&amp;nbsp;있습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. RemoteMediator 코드 분석 정리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 안드로이드 Jetpack Compose Paging3 라이브러리에서 제공하는 RemoteMediator의 코드 분석을 통해 데이터 페이징 처리의 세밀한 조정과 응용이 가능해진다고 생각해서 코드를 분석하고 정리했습니다. &lt;br /&gt;&lt;br /&gt;특히, 네트워크와 로컬 데이터베이스 간의 동기화를 효과적으로 관리함으로써, 오프라인 지원이 가능한 더욱 견고한 앱을 만들 수 있습니다. &lt;br /&gt;&lt;br /&gt;RemoteMediator와 PagingSource를 Pager 객체에 전달하고, 데이터가 UI에 보이는 모습까지 확인하겠습니다.&lt;/p&gt;</description>
      <category>안드로이드/Compose Paging3</category>
      <category>Jetpack Compose</category>
      <category>pager</category>
      <category>paging3</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/25</guid>
      <comments>https://code-tales.tistory.com/25#entry25comment</comments>
      <pubDate>Wed, 28 Feb 2024 11:30:27 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 Jetpack Compose Paging3 시리즈] RemoteMediator 구현하기 #9</title>
      <link>https://code-tales.tistory.com/24</link>
      <description>&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. RemoteMediator 구현하기&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;안녕하세요. 지난 포스팅에서는 안드로이드 Jetpack Compose Paging3 라이브러리를 사용할 때 로컬 데이터베이스와 상호작용하기 위한 Local Data Source를 구현했습니다.&lt;br /&gt;&lt;br /&gt;이번 포스팅에서는 네트워크를 통해 가져온 데이터를 로컬 데이터베이스에 저장하기 위해 RemoteMediator를 구현하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;555&quot; data-origin-height=&quot;734&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzzj6z/btsFeDFeqCw/mgvLgFAdHf7qk3i8GxyC5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzzj6z/btsFeDFeqCw/mgvLgFAdHf7qk3i8GxyC5K/img.png&quot; data-alt=&quot;MovieLocalDataSource와 MovieRemoteMediator를 추가한 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzzj6z/btsFeDFeqCw/mgvLgFAdHf7qk3i8GxyC5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbzzj6z%2FbtsFeDFeqCw%2FmgvLgFAdHf7qk3i8GxyC5K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;MovieLocalDataSource와 MovieRemoteMediator를 추가한 모습&quot; loading=&quot;lazy&quot; width=&quot;279&quot; height=&quot;369&quot; data-origin-width=&quot;555&quot; data-origin-height=&quot;734&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;MovieLocalDataSource와 MovieRemoteMediator를 추가한 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. RemoteMediator란?&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;RemoteMediator는 안드로이드 Jetpack Compose의 Paging3 라이브러리의 핵심 컴포넌트 중 하나로, 네트워크와 로컬 데이터베이스 간의 데이터 동기화를 담당합니다. 네트워크에서 데이터를 가져온 후 로컬 데이터베이스에 저장하고, 이 데이터를 PagingSource를 통해 UI에 제공하는 역할을 합니다. RemoteMediator를 사용함으로써, 앱은 네트워크 연결 상태에 관계없이 지속적으로 데이터를 제공할 수 있으며, 데이터의 캐싱과 동기화를 효율적으로 관리할 수 있습니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-1. RemoteMediator 생성&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;네트워크와 로컬 데이터베이스의 동기화를 진행하기 위해서 RemoteMediator를 만들겠습니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;네트워크를 통해 가져온 데이터를 로컬 데이터베이스에 저장하기 위해서 지난 포스팅에서 구현한 LocalDataSource를 사용합니다.&lt;/b&gt;&lt;/span&gt; 아래 코드는 RemoteMediator 추상 클래스를 구현한 MovieRemoteMediator입니다. 스크롤 상태에 따라 page 정보를 얻어오고 있으며, page 정보를 이용하여 네트워크 통신을 하고 있습니다. 그 후, local.saveMovies() 함수의 호출을 통해 로컬 데이터베이스에 데이터를 저장합니다.&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// MovieRemoteMediator.kt

private const val MOVIE_START_PAGE_INDEX = 1

@OptIn(ExperimentalPagingApi::class)
class MovieRemoteMediator(
    private val local: MovieDataSource.Local,
    private val remote: MovieDataSource.Remote
) : RemoteMediator&amp;lt;Int, MovieDbData&amp;gt;() {
    
    override suspend fun load(loadType: LoadType, state: PagingState&amp;lt;Int, MovieDbData&amp;gt;): MediatorResult {
        val page = when (loadType) {
            LoadType.REFRESH -&amp;gt; MOVIE_START_PAGE_INDEX
            LoadType.PREPEND -&amp;gt; return MediatorResult.Success(endOfPaginationReached = true)
            LoadType.APPEND -&amp;gt; local.getLastRemoteKey()?.nextPage ?: return MediatorResult.Success(endOfPaginationReached = true)
        }

        remote.getMovies(page, state.config.pageSize).getResult({ successResult -&amp;gt;
            if (loadType == LoadType.REFRESH) {
                local.clearMovies()
                local.clearRemoteKeys()
            }

            val movies = successResult.data

            val endOfPaginationReached = movies.isEmpty()

            val prevPage = if (page == MOVIE_START_PAGE_INDEX) null else page - 1
            val nextPage = if (endOfPaginationReached) null else page + 1

            val key = MovieRemoteKeyDbData(prevPage = prevPage, nextPage = nextPage)

            local.saveMovies(movies)
            local.saveRemoteKey(key)

            return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)
        }, { errorResult -&amp;gt;
            return MediatorResult.Error(errorResult.error)
        })
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;3. RemoteMediator 적용 정리&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 LocalDataSource와 RemoteMediator를 구현했습니다. 이제 Pager 객체에 RemoteMediator와 PagingSource만 제공하면 Paging3 라이브러리가 자동으로 페이징 데이터를 전달합니다.&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;다음 포스팅에서는 Paging3 라이브러리에서 제공하는 RemoteMediator에 있는 LoadType.REFRESH, LoadType.PREPEND, LoadType.APPEND, MediatorResult 등에 대해 설명하겠습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>안드로이드/Compose Paging3</category>
      <category>Jetpack Compose</category>
      <category>pager</category>
      <category>paging3</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/24</guid>
      <comments>https://code-tales.tistory.com/24#entry24comment</comments>
      <pubDate>Tue, 27 Feb 2024 11:30:04 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 Jetpack Compose Paging3 시리즈] Local Data Source 생성하기 #8</title>
      <link>https://code-tales.tistory.com/23</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 로컬 데이터베이스에 접근하기 위한 Local Data Source&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 지난 포스팅에서는 안드로이드 Jetpack Compose Paging3 라이브러리를 사용할 때 로컬 데이터베이스에 캐시 데이터를 저장하기 위해서 Room 라이브러리를 이용하여 데이터베이스를 세팅해 주었습니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;이번 포스팅에서는 데이터베이스와 상호작용을 하기 위한 Local Data Source를 세팅하겠습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;631&quot; data-origin-height=&quot;838&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMRfzk/btsEYJSNWXk/XRjCs1lPMYBIQm6v6RBSAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMRfzk/btsEYJSNWXk/XRjCs1lPMYBIQm6v6RBSAk/img.png&quot; data-alt=&quot;안드로이드 Jetpack Compose의 Paging3 라이브러리에서 RemoteMediator를 사용하기 위한 Local Data Source 관련 파일&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMRfzk/btsEYJSNWXk/XRjCs1lPMYBIQm6v6RBSAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMRfzk%2FbtsEYJSNWXk%2FXRjCs1lPMYBIQm6v6RBSAk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;Local Data Source 관련 파일&quot; loading=&quot;lazy&quot; width=&quot;351&quot; height=&quot;466&quot; data-origin-width=&quot;631&quot; data-origin-height=&quot;838&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;안드로이드 Jetpack Compose의 Paging3 라이브러리에서 RemoteMediator를 사용하기 위한 Local Data Source 관련 파일&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. Local Data Source 세팅하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RemoteMediator는 안드로이드 Jetpack Compose의 Paging3 라이브러리의 핵심 컴포넌트 중 하나로, 네트워크와 로컬 데이터베이스 간의 데이터 동기화를 담당합니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;그래서 데이터베이스와 상호작용을 할 수 있는 Local Data Source를 먼저 구현해주어야 합니다. 네트워크에서 데이터를 가져온 후 로컬 데이터베이스에 저장하고, 이 데이터를 PagingSource를 통해 UI에 제공하는 역할을 하기 때문입니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-1. RemoteMediator에서 사용할 LocalDataSource 생성&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 포스팅에서 생성한 데이터베이스에 접근할 수 있는 Local Data Source를 생성하겠습니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Data Source는 클린 아키텍처의 Data Layer에서 네트워크나 로컬 데이터베이스와의 연결을 담당합니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// MovieDataSource.kt
interface MovieDataSource {
    interface Remote {
        suspend fun getMovies(page: Int, limit: Int): ApiResult&amp;lt;List&amp;lt;MovieEntity&amp;gt;&amp;gt;
        suspend fun getMovie(movieId: Int): ApiResult&amp;lt;MovieEntity&amp;gt;
    }

    interface Local {
        fun pagingSource(): PagingSource&amp;lt;Int, MovieDbData&amp;gt;
        suspend fun getMovies(): ApiResult&amp;lt;List&amp;lt;MovieEntity&amp;gt;&amp;gt;
        suspend fun saveMovies(movieEntities: List&amp;lt;MovieEntity&amp;gt;)
        suspend fun getLastRemoteKey(): MovieRemoteKeyDbData?
        suspend fun saveRemoteKey(key: MovieRemoteKeyDbData)
        suspend fun clearMovies()
        suspend fun clearRemoteKeys()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// MovieDataMapper.kt
fun MovieEntity.toDbData() = MovieDbData(
    id = id,
    image = image,
    description = description,
    title = title,
    category = category,
    backgroundUrl = backgroundUrl
)

// DiskExecutor.kt
class DiskExecutor : Executor {
    private val executor: Executor = Executors.newSingleThreadExecutor()
    override fun execute(runnable: Runnable) {
        executor.execute(runnable)
    }
}

// MovieLocalDataSource.kt
class MovieLocalDataSource(
    private val executor: DiskExecutor,
    private val movieDao: MovieDao,
    private val remoteKeyDao: MovieRemoteKeyDao,
) : MovieDataSource.Local {

    override fun pagingSource(): PagingSource&amp;lt;Int, MovieDbData&amp;gt; = movieDao.pagingSource()

    override suspend fun getMovies(): ApiResult&amp;lt;List&amp;lt;MovieEntity&amp;gt;&amp;gt; = withContext(executor.asCoroutineDispatcher()) {
        val movies = movieDao.getMovies()
        return@withContext if (movies.isNotEmpty()) {
            ApiResult.Success(movies.map { it.toDomain() })
        } else {
            ApiResult.Error(Throwable(&quot;Data Not Available&quot;))
        }
    }
    override suspend fun saveMovies(movieEntities: List&amp;lt;MovieEntity&amp;gt;) = withContext(executor.asCoroutineDispatcher()) {
        movieDao.saveMovies(movieEntities.map { it.toDbData() })
    }

    override suspend fun getLastRemoteKey(): MovieRemoteKeyDbData? = withContext(executor.asCoroutineDispatcher()) {
        remoteKeyDao.getLastRemoteKey()
    }

    override suspend fun saveRemoteKey(key: MovieRemoteKeyDbData) = withContext(executor.asCoroutineDispatcher()) {
        remoteKeyDao.saveRemoteKey(key)
    }

    override suspend fun clearMovies() = withContext(executor.asCoroutineDispatcher()) {
        movieDao.clearMoviesExceptFavorites()
    }

    override suspend fun clearRemoteKeys() = withContext(executor.asCoroutineDispatcher()) {
        remoteKeyDao.clearRemoteKeys()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-2. MovieRepository 생성자에 Local Data Source 추가하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;MovieRepository에서 Local Data Source에 접근할 수 있도록, 위에서 정의한 Local Data Source를 생성자에 추가합니다. 현재 movies 함수는 이전에 구현했던 네트워크 통신 기반의 페이징 처리를 구현한 부분인데 다음 포스팅에서 RemoteMediator를 적용한 함수로 변경할 예정입니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;class MovieRepositoryImpl(
    private val remote: MovieDataSource.Remote,
    private val local: MovieDataSource.Local,
) : MovieRepository {
    override fun movies(pageSize: Int): Flow&amp;lt;PagingData&amp;lt;MovieEntity&amp;gt;&amp;gt; {
        return Pager(
            config = PagingConfig(
                pageSize = pageSize,
                enablePlaceholders = false,
            ),
            remoteMediator = // RemoteMediator 전달해야함
            pagingSourceFactory = { MoviesPagingSource(remote) }
        ).flow
    }

    override suspend fun getMovieDetail(movieId: Int): ApiResult&amp;lt;MovieEntity&amp;gt; =
        remote.getMovie(movieId)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-3. Local Data Source 관련 의존성 주입하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성자로 전달받는 Local Data Source, MovieDao 등을 사용하기 위해서 의존성 주입을 해주어야 합니다. 본 시리즈는 클린 아키텍처 시리즈의 프로젝트에서 이어서 하는 중이기 때문에 Hilt 모듈을 추가하겠습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// DatabaseModule.kt (UI Layer)
@Module
@InstallIn(SingletonComponent::class)
class DatabaseModule {
    @Provides
    @Singleton
    fun provideMovieDatabase(@ApplicationContext context: Context): MovieDatabase {
        return Room.databaseBuilder(context, MovieDatabase::class.java, &quot;movie.db&quot;).build()
    }

    @Provides
    fun provideMovieDao(movieDatabase: MovieDatabase): MovieDao {
        return movieDatabase.movieDao()
    }

    @Provides
    fun provideMovieRemoteKeyDao(movieDatabase: MovieDatabase): MovieRemoteKeyDao {
        return movieDatabase.movieRemoteKeyDao()
    }
}

// DataModule.kt (UI Layer)
@Module
@InstallIn(SingletonComponent::class)
class DataModule {
    @Provides
    fun provideDiskExecutor(): DiskExecutor {
        return DiskExecutor()
    }

    @Provides
    @Singleton
    fun provideMovieRepository(
        movieRemote: MovieDataSource.Remote,
        movieLocal: MovieDataSource.Local,
    ): MovieRepository {
        return MovieRepositoryImpl(
            remote = movieRemote,
            local = movieLocal
        )
    }


    @Provides
    @Singleton
    fun provideMovieLocalDataSource(
        executor: DiskExecutor,
        movieDao: MovieDao,
        movieRemoteKeyDao: MovieRemoteKeyDao,
    ): MovieDataSource.Local {
        return MovieLocalDataSource(executor, movieDao, movieRemoteKeyDao)
    }
    
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. Local Data Source 정리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 안드로이드 Jetpack Compose Paging3에서 RemoteMediator를 구현하기 위한 사전 작업을 해주었습니다. 지난 포스팅에서 세팅한 Room 데이터베이스를 사용하기 위해서 Local Data Source를 만들고 의존성 주입까지 해주었습니다.&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;이제 Paging3 라이브러리의 RemoteMediator를 구현하는 과정만 남았습니다.&lt;/b&gt; &lt;br /&gt;&lt;/span&gt;다음 포스팅에 이어서 진행하겠습니다.&lt;/p&gt;</description>
      <category>안드로이드/Compose Paging3</category>
      <category>Jetpack Compose</category>
      <category>pager</category>
      <category>paging3</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/23</guid>
      <comments>https://code-tales.tistory.com/23#entry23comment</comments>
      <pubDate>Mon, 26 Feb 2024 11:30:30 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 Jetpack Compose Paging3 시리즈] Room Database 세팅하기 #7</title>
      <link>https://code-tales.tistory.com/22</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. RemoteMediator 구현을 위해 Room Database 세팅&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 이번 포스팅에서는 Jetpack Compose Paging3 라이브러리에서 RemoteMediator를 구현하기 위해 Room Database 세팅을 진행하겠습니다. 지난 포스팅에서 설명했듯이, RemoteMediator는 Paging3 라이브러리로 불러온 데이터를 로컬 데이터베이스에 저장한다고 말씀드렸습니다. 그래서 먼저 Room Database를 세팅한 후, RemoteMediator를 구현해야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;394&quot; data-origin-height=&quot;391&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tyfn5/btsEWJZJqjX/gAhUD8f68kf2FkoxARy9Tk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tyfn5/btsEWJZJqjX/gAhUD8f68kf2FkoxARy9Tk/img.png&quot; data-alt=&quot;Room Database 세팅을 위한 파일 생성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tyfn5/btsEWJZJqjX/gAhUD8f68kf2FkoxARy9Tk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ftyfn5%2FbtsEWJZJqjX%2FgAhUD8f68kf2FkoxARy9Tk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;Room Database 세팅을 위한 파일 생성&quot; loading=&quot;lazy&quot; width=&quot;360&quot; height=&quot;357&quot; data-origin-width=&quot;394&quot; data-origin-height=&quot;391&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Room Database 세팅을 위한 파일 생성&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. Data Layer에 데이터베이스 관련 클래스 및 인터페이스 생성&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Data Layer에 Room 데이터베이스 구성을 위한 파일들을 생성하겠습니다. 어렵게 생각하지 말고 간단하게 생각하면 쉽습니다. 데이터베이스가 구성되기 위해서는 Database, Table, DAO, DTO 등이 필요합니다. 저는 에러를 피하고 자동완성을 활용하기 위해 역방향으로 클래스 및 인터페이스를 생성하겠습니다.&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;아래 코드를 보면 RemoteKey 관련된 파일을 생성한 것을 확인할 수 있는데, 제가 사용하는 API가 page를 파라미터로 받아서 데이터를 돌려주기 때문에 마지막 페이지를 저장하기 위해서 만들었습니다.&lt;br /&gt;&lt;br /&gt;만약, page 기반이 아닌 마지막 아이템 ID를&amp;nbsp;기준으로 페이징 처리를 하는 cursor 기반의 API라면 RemoteKey 저장은 하지 않아도 됩니다. RemoteMediator에서 마지막 아이템을 조회할 수 있기 때문입니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-1. DTO(Data Transfer Object) 만들기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DTO는 계층 간에 데이터를 주고받기 위해서 사용하는 객체입니다. 그래서 DTO는 로직을 갖지 않는 data class 역할만 합니다. Room 데이터베이스에서는 data class에 @Entity 어노테이션을 추가해서 테이블을 생성할 수 있습니다. 여기서는 테이블을 만들 때 사용한 data class가 곧 DTO의 역할도 하게 됩니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// MovieDbData.kt
@Entity(tableName = &quot;movies&quot;)
data class MovieDbData(
    @PrimaryKey val id: Int,
    val description: String,
    val image: String,
    val backgroundUrl: String,
    val title: String,
    val category: String,
)

fun MovieDbData.toDomain() = MovieEntity(
    id = id,
    title = title,
    description = description,
    image = image,
    category = category,
    backgroundUrl = backgroundUrl
)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// MovieRemoteKeyDbData.kt
@Entity(tableName = &quot;movies_remote_keys&quot;)
data class MovieRemoteKeyDbData(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    val prevPage: Int?,
    val nextPage: Int?
)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-2. DAO(Data Access Object) 만들기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DAO는 데이터베이스에 접근해서 데이터를 저장하거나 가져오는 역할을 하는 객체입니다. Room 라이브러리에서는 interface로 정의하면 됩니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;아래 MovieDao의 pagingSource 함수를 보면 반환타입이 PagingSource인 것을 확인할 수 있습니다. Room에서 PagingSource를 반환하게 되면 Pager 객체가 해당 테이블을 페이징 된 데이터 소스로 사용할 수 있습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// MovieDao.kt
@Dao
interface MovieDao {
    @Query(&quot;SELECT * FROM movies ORDER BY category, id&quot;)
    fun pagingSource(): PagingSource&amp;lt;Int, MovieDbData&amp;gt;
    
    @Query(&quot;SELECT * FROM movies ORDER BY category,id&quot;)
    fun getMovies(): List&amp;lt;MovieDbData&amp;gt;
    
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun saveMovies(movies: List&amp;lt;MovieDbData&amp;gt;)
    
    @Query(&quot;DELETE FROM movies&quot;)
    suspend fun clearMoviesExceptFavorites()
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// MovieRemoteKeyDao.kt
@Dao
interface MovieRemoteKeyDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun saveRemoteKey(keys: MovieRemoteKeyDbData)

    @Query(&quot;SELECT * FROM movies_remote_keys WHERE id=:id&quot;)
    suspend fun getRemoteKeyByMovieId(id: Int): MovieRemoteKeyDbData?

    @Query(&quot;DELETE FROM movies_remote_keys&quot;)
    suspend fun clearRemoteKeys()

    @Query(&quot;SELECT * FROM movies_remote_keys WHERE id = (SELECT MAX(id) FROM movies_remote_keys)&quot;)
    suspend fun getLastRemoteKey(): MovieRemoteKeyDbData?
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-3. MovieDatabase 만들기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 생성한 Table, DTO, DAO를 이용해서 Database 클래스를 만들겠습니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Database도 마찬가지로 @Database 어노테이션을 추가해서 생성할 수 있습니다. 이때, entities 프로퍼티에 위에서 만든 Table인 MovieDbData, MovieRemoteKeyDbData를 넣어줍니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// MovieDatabase.kt
@Database(
    entities = [MovieDbData::class, MovieRemoteKeyDbData::class],
    version = 1,
    exportSchema = false
)
abstract class MovieDatabase: RoomDatabase() {
    abstract fun movieDao(): MovieDao
    abstract fun movieRemoteKeyDao(): MovieRemoteKeyDao
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. Room Database 내용 정리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 안드로이드 Jetpack Compose Paging3 라이브러리의 RemoteMediator 구현을 위해서 Room database에 필요한 클래스 및 인터페이스를 정의했습니다.&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;MovieDao를 보면 PagingSource를 반환하는 pagingSource 함수를 만들었는데, Pager 객체를 생성할 때 pagingSourceFactory로 해당 함수를 제공하면 Paging3 라이브러리가 해당 테이블을 페이징 된 데이터 소스로 사용할 수 있습니다.&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;다음 포스팅에서는 RemoteMediator를 구현하고 Pager 객체를 생성하겠습니다.&lt;/p&gt;</description>
      <category>안드로이드/Compose Paging3</category>
      <category>Jetpack Compose</category>
      <category>pager</category>
      <category>paging3</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/22</guid>
      <comments>https://code-tales.tistory.com/22#entry22comment</comments>
      <pubDate>Sun, 25 Feb 2024 11:30:52 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 Jetpack Compose Paging3 시리즈] RemoteMediator 파헤치기 #6</title>
      <link>https://code-tales.tistory.com/21</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 네트워크, 데이터베이스에서 페이징을 구현하기 위한 RemoteMediator&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 지난 포스팅에서는 네트워크 통신만을 이용해서 페이징 기능을 구현했었습니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;이번 시간에는 안드로이드 Jetpack Compose Paging3 시리즈에서 개념이 어렵다고 생각하는 RemoteMediator의 원리 및 사용방법에 대해서 설명하고, 프로젝트에 RemoteMediator를 적용해 보는 과정을 설명하겠습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;img.png&quot; data-origin-width=&quot;1237&quot; data-origin-height=&quot;338&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buEE99/btsFeR4kfBz/7LZgdI3f4DXgw9laly1TK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buEE99/btsFeR4kfBz/7LZgdI3f4DXgw9laly1TK0/img.png&quot; data-alt=&quot;Pager, PagingSource, RemoteMediator 생명주기 다이어그램&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buEE99/btsFeR4kfBz/7LZgdI3f4DXgw9laly1TK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuEE99%2FbtsFeR4kfBz%2F7LZgdI3f4DXgw9laly1TK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;Pager&amp;amp;#44; PagingSource&amp;amp;#44; RemoteMediator 생명주기 다이어그램&quot; loading=&quot;lazy&quot; width=&quot;1237&quot; height=&quot;338&quot; data-filename=&quot;img.png&quot; data-origin-width=&quot;1237&quot; data-origin-height=&quot;338&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Pager, PagingSource, RemoteMediator 생명주기 다이어그램&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. RemoteMediator 원리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 Jetpack Compose Paging3 라이브러리는 RemoteMediator를 제공합니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;RemoteMediator는 네트워크를 통해 가져온 데이터를 페이징 데이터로 활용하는 것이 아니라 로컬 데이터베이스(Room)에 저장합니다. 이렇게 로컬 데이터베이스에 저장된 데이터를 PagingSource에서 불러오고 UI로 제공해 주게 됩니다.&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;즉, 아래의 그림과 같이 Pager가 모든 페이징 데이터 보여줬거나 기존 데이터가 없을 때, 다음 페이지에 대한 항목들을 보여주기 위해 RemoteMediator를 통해 네트워크에서 다음 데이터를 불러옵니다. 그리고 가져온 데이터들을 로컬 데이터베이스(Room)에 저장하면&amp;nbsp;UI가 자동으로 업데이트되는 원리입니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-1. RemoteMediator 구현 과정의 간략한 개요&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;RemoteMediator를 구현하는 과정은 크게 아래 4단계로 요약할 수 있습니다. 이 과정은 네트워크에서 데이터를 가져와 로컬 데이터베이스에 저장하고, 이 데이터를 앱의 UI에 표시합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;네트워크 요청&lt;/span&gt;:&lt;/b&gt; RemoteMediator는 네트워크 API를 호출하여 데이터를 가져옵니다. 이 과정에서는 페이징 처리가 필요한 데이터의 다음 페이지를 요청합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;데이터 저장&lt;/span&gt;:&lt;/b&gt; 네트워크로부터 받은 데이터를 로컬 데이터베이스(Room)에 저장합니다. 이 단계는 데이터의 영속성을 보장하고, 오프라인 접근을 가능하게 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;데이터 읽기&lt;/span&gt;:&lt;/b&gt; UI가 데이터를 필요로 할 때, PagingSource는 로컬 데이터베이스에서 데이터를 읽어와 PagingData 형태로 UI에 제공합니다. 이는 데이터의 일관된 표시를 보장합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;UI 업데이트&lt;/span&gt;:&lt;/b&gt; 로컬 데이터베이스에 새로운 데이터가 추가되거나 변경되면, Paging3 라이브러리는 자동으로 UI를 업데이트합니다. 즉, 사용자에게 최신 데이터를 실시간으로 보여줄 수 있게 합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-2. RemoteMediator 장점&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이전 포스팅에서는 RemoteMediator를 제외하고 네트워크 통신만을 이용해서 페이징 기능을 적용했었습니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;RemoteMediator를 구현하지 않아도 애플리케이션을 사용하는데 문제는 없지만, 그럼에도 불구하고 RemoteMediator를 구현했을 때의 장점에 대해서 정리했습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;효율적인 네트워크 사용이 가능해집니다.&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;데이터를 로컬에 캐싱함으로써, 같은 요청에 대한 네트워크 호출을 반복하지 않아도 되기 때문입니다. 즉, 데이터 사용량을 줄이고 반응 속도를 향상할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;오프라인(비행기 모드 등)에서도 로컬에 캐싱된 데이터는 볼 수 있습니다.&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;네트워크 접근성이 낮은 지역에서의 사용성을 크게 향상시킬 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;로컬 데이터베이스에 데이터가 추가되면 Paging3 라이브러리가 자동으로 UI를 업데이트합니다.&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;RemoteMediator를 구현하지 않았을 때와 마찬가지로 flow 함수를 통해 데이터 스트림을 만들 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;개발자가 캐시 된 데이터가 오래되었는지 확인하고, 최신 데이터로의 갱신 여부를 구현하기 쉽습니다.&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. RemoteMediator 내용 정리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 안드로이드 Jetpack Compose Paging3 라이브러리의 RemoteMediator의 원리와 장점에 대해서 정리했습니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;RemoteMediator를 구현하면 네트워크와 로컬 데이터베이스를 사용하는 복잡한 페이징 로직을 효율적으로 관리하고, 사용자에게 더 나은 경험을 제공할 수 있게 됩니다.&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;원래대로라면 RemoteMediator를 구현하는 과정을 담으려고 했지만, 헷갈리는 요소들이 있다고 생각해서 이론에 대해 먼저 포스팅하게 되었습니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;다음 포스팅에서는 오늘 정리한 내용을 기반으로RemoteMediator를 실제 프로젝트에 적용하면서 자세히 살펴보겠습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>안드로이드/Compose Paging3</category>
      <category>Jetpack Compose</category>
      <category>pager</category>
      <category>paging3</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/21</guid>
      <comments>https://code-tales.tistory.com/21#entry21comment</comments>
      <pubDate>Sat, 24 Feb 2024 11:30:21 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 Jetpack Compose Paging3 시리즈] LazyVerticalGrid 구현 및 Navigation #5</title>
      <link>https://code-tales.tistory.com/20</link>
      <description>&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. LazyColumn을 LazyVerticalGrid로 변경&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 이번 포스팅에서는 안드로이드 Jetpack Compose Paging3 라이브러리에서 불러온 데이터를 LazyColumn에서 LazyVerticalGrid로 변경하는 과정에 대해 알아보고, 영화 아이템을 클릭했을 때 클린 아키텍처 시리즈에서 구현한 영화 상세 화면으로 이동하는 기능을 Navigation을 통해 구현하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;move_list.png&quot; data-origin-width=&quot;216&quot; data-origin-height=&quot;444&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kZuGR/btsEORCSayD/ktrB3zdf7mVn7YnBvELJrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kZuGR/btsEORCSayD/ktrB3zdf7mVn7YnBvELJrK/img.png&quot; data-alt=&quot;LazyVerticalGrid로 구현한 영화 목록 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kZuGR/btsEORCSayD/ktrB3zdf7mVn7YnBvELJrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkZuGR%2FbtsEORCSayD%2FktrB3zdf7mVn7YnBvELJrK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;LazyVerticalGrid로 구현한 영화목록화면&quot; loading=&quot;lazy&quot; width=&quot;216&quot; height=&quot;444&quot; data-filename=&quot;move_list.png&quot; data-origin-width=&quot;216&quot; data-origin-height=&quot;444&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;LazyVerticalGrid로 구현한 영화 목록 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1-1. LazyVerticalGrid 설명 및 구현&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LazyColumn이 리스트 형태의 데이터를 세로로 표시하는 데 사용되는 것과 달리, LazyVerticalGrid는 격자(grid) 형태로 데이터를 표시할 때 사용됩니다. 이를 통해 사용자에게 더 다양한 형태의 UI를 제공할 수 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;이제 Lazy&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;Column에서 구현했던 목록을 LazyVerticalGrid로 변경하겠습니다. Paging3 시리즈를 순서대로 보신 분들이라면 Paging3 라이브러리와 함&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;께 사용하던 코드를 그대로 유지하되, UI 구성 부분만 변경하면 됩니다.&lt;/span&gt; &lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MovieListScreen(
    mainNavController: NavHostController,
    movies: LazyPagingItems&amp;lt;MovieEntity&amp;gt;,
) {
    val favoriteIcon = drawable.ic_favorite_fill_white // drawable.ic_favorite_border_white

    Scaffold(
        floatingActionButton = {
            FloatingActionButton(onClick = { /*TODO*/ }) {
                Image(
                    painter = painterResource(id = favoriteIcon),
                    contentDescription = null,
                    Modifier.size(24.dp),
                )
            }
        },
        topBar = {
            TopAppBar(
                title = { Text(text = &quot;영화 목록&quot;) },
                navigationIcon = {
                    IconButton(onClick = { mainNavController.popBackStack() }) {
                        Icon(imageVector = Icons.Default.ArrowBack, contentDescription = null)
                    }
                }
            )
        }
    ) { paddingValues -&amp;gt;
        LazyVerticalGrid(
            modifier = Modifier.padding(paddingValues),
            columns = GridCells.Fixed(3),
            state = rememberLazyGridState()
        ) {
            items(movies.itemCount, span = { index -&amp;gt;
                GridItemSpan(1)
            }) { index -&amp;gt;
                val movie = movies[index]
                MovieItem(movie!!, ImageSize.getImageFixedSize()) { movieId -&amp;gt;
                    mainNavController.navigate(Page.MovieDetail.route + &quot;/${movieId}&quot;)
                }
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. Navigation 구성&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 과정을 통해 LazyVerticalGrid를 사용해서 영화 목록을 구현했다면, 영화를 눌렀을 때 해당 영화의 상세 화면으로 이동해 주는 기능을 구현하겠습니다. 클린 아키텍처 시리즈에서 Compose Navigation을 구현했기 때문에 NavGraph를 설정하고 NavController를 통해 화면을 이동하겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-1. NavGraph에 경로 추가&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 구현했던 MainGraph에 MovieDetailPage Composable을 추가합니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;영화 아이템을 누르면 해당 Id값을 영화 상세 화면으로 전달해주어야 하기 때문에 argument 인자에 movieId에 대한 정의와 route 인자에 {movieId}를 추가해 줍니다.&lt;/b&gt;&lt;/span&gt; 이렇게 하면 NavController의 navigate 함수를 호출할 때, route 경로에 전달한 영화 Id값을 받을 수 있습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;sealed class Page(val route: String) {
    data object Movies: Page(&quot;movies&quot;)
    data object MovieDetail: Page(&quot;movie_detail&quot;)
}

@Composable
fun MainGraph(
    mainNavController: NavHostController
) {
    NavHost(navController = mainNavController, startDestination = Page.Movies.route) {
        composable(
            route = Page.Movies.route
        ) {
            val viewModel = hiltViewModel&amp;lt;MovieListViewModel&amp;gt;()
            MovieListPage(
                mainNavController = mainNavController,
                viewModel = viewModel
            )
        }

        composable(
            route = &quot;${Page.MovieDetail.route}/{movieId}&quot;,
            arguments = listOf(navArgument(&quot;movieId&quot;) {
                type = NavType.IntType
                defaultValue = 0
                nullable = false
            })
        ) {
            val viewModel = hiltViewModel&amp;lt;MovieDetailViewModel&amp;gt;()
            MovieDetailPage(
                mainNavController = mainNavController,
                viewModel = viewModel
            )
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-2. NavController를 통해 화면 이동&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영화 아이템을 눌렀을 때 해당 영화의 상세 화면으로 이동되어야 합니다.&lt;br /&gt;&lt;br /&gt;1번 과정에서 구현했던 LazyVerticalGrid에서 사용하는 MovieItem Composable에서 onMovieClick 함수를 통해 해당 영화의 Id 값을 전달하도록 구현했습니다. &lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;onMovieClick 함수의 구현부에서는 전달받은 영화 Id를 사용해서 NavController를 통해 화면 이동을 실행합니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
private fun MovieItem(
    movie: MovieEntity,
    imageSize: ImageSize,
    onMovieClick: (movieId: Int) -&amp;gt; Unit = {}
) {
    SubcomposeAsyncImage(
        model = movie.image,
        loading = { MovieItemPlaceholder() },
        error = { MovieItemPlaceholder() },
        contentDescription = null,
        contentScale = ContentScale.Crop,
        modifier = Modifier
            .padding(3.dp)
            .size(imageSize.width, imageSize.height)
            .clickable { onMovieClick(movie.id) }
            .clip(RoundedCornerShape(2))
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;MovieItem(movie!!, ImageSize.getImageFixedSize()) { movieId -&amp;gt;
	mainNavController.navigate(Page.MovieDetail.route + &quot;/${movieId}&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. LazyVerticalGrid 구현 및 Navig&lt;/b&gt;&lt;b&gt;ation 정리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 안드로이드 Jetpack Compose의 LazyColumn에서 LazyVerticalGrid로의 전환, 그리고 영화 아이템을 클릭했을 때 해당 영화의 상세화면으로 이동하는 기능에 대해서 알아보았습니다. LazyColumn과 LazyVerticalGrid 모두 Paging3 라이브러리와 호환이 되며 무한스크롤을 지원합니다. 구현하고자 하는 화면 디자인에 맞춰서 적절한 composable을 사용하는데 도움이 되었으면 좋겠습니다.&lt;br /&gt;&lt;br /&gt;다음 포스팅에서는 로컬 데이터베이스를 사용해서 캐시 저장을 하기 위한 작업을 할 예정입니다. RemoteMediator를 추가하기에 앞서, 로컬 데이터베이스를 Room 라이브러리를 통해 구현하겠습니다.&lt;/p&gt;</description>
      <category>안드로이드/Compose Paging3</category>
      <category>Jetpack Compose</category>
      <category>pager</category>
      <category>paging3</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/20</guid>
      <comments>https://code-tales.tistory.com/20#entry20comment</comments>
      <pubDate>Fri, 23 Feb 2024 11:30:50 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드 Jetpack Compose Paging3 시리즈] LazyColumn 구현 #4</title>
      <link>https://code-tales.tistory.com/19</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. PagingData, LazyColumn을 사용한 무한 스크롤 리스트 구현&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 지난 시간에는 안드로이드 Jetpack Compose의 Paging3 라이브러리를 사용하기 위해 PagingSource와 PagingConfig를 활용하여 네트워크에서 데이터를 페이징 처리하는 방법을 살펴보았습니다. 이번 포스팅에서는 안드로이드 Jetpack Compose의 LazyColumn을 사용해서 Paging3 라이브러리에서 불러온 데이터를 사용자에게 보여주는 기능을 구현하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot_20240212_184143.png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2220&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uxEck/btsEHI6WXUM/7k8lacGkr4xMoVVUWY4wgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uxEck/btsEHI6WXUM/7k8lacGkr4xMoVVUWY4wgk/img.png&quot; data-alt=&quot;Paging3, LazyColumn을 통해 무한스크롤 리스트 구현&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uxEck/btsEHI6WXUM/7k8lacGkr4xMoVVUWY4wgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuxEck%2FbtsEHI6WXUM%2F7k8lacGkr4xMoVVUWY4wgk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;Paging3&amp;amp;#44; LazyColumn을 통해 무한스크롤 리스트 UI를 구현한 모습&quot; loading=&quot;lazy&quot; width=&quot;207&quot; height=&quot;425&quot; data-filename=&quot;Screenshot_20240212_184143.png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2220&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Paging3, LazyColumn을 통해 무한스크롤 리스트 구현&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1-1. LazyColumn 개념&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LazyColumn은 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Jetpack Compose에서 제공하는 컴포저블 함수로, 화면에 보이는 부분만 렌더링 하기 때문에 RecyclerView와 마찬가지로 리스트를 효율적으로 표시할 수 있게 해주는 컴포넌트입니다.&lt;/b&gt;&lt;/span&gt; Paging3 라이브러리를 사용해서 RecyclerView를 구현하려면 layout, Adapter 등의 선행 작업이 필요했지만, Jetpack Compose의 LazyColumn은 이러한 선행 작업이 필요 없어서 편한 장점이 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1-2. PagingData와&amp;nbsp;LazyColumn&amp;nbsp;연동&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Paging3 라이브러리와 Jetpack Compose를 함께 사용할 때는, &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;collectAsLazyPagingItems() 확장 함수를 사용하여 PagingData를 LazyPagingItems로 변환한 후, 이를 LazyColumn에서 사용할 수 있습니다.&lt;/b&gt;&lt;/span&gt; RecyclerView를 구현할 때보다 코드가 엄청 줄어든 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun MovieListScreen(
    mainNavController: NavHostController,
    viewModel: MovieListViewModel
) {
    val movies = viewModel.movies.collectAsLazyPagingItems()
    MovieListScreen(mainNavController, movies)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun MovieListScreen(
    mainNavController: NavHostController,
    movies: LazyPagingItems&amp;lt;MovieEntity&amp;gt;
) {
    val favoriteIcon = drawable.ic_favorite_fill_white // drawable.ic_favorite_border_white

    Scaffold(
        floatingActionButton = {
            FloatingActionButton(onClick = { /*TODO*/ }) {
                Image(
                    painter = painterResource(id = favoriteIcon),
                    contentDescription = null,
                    Modifier.size(24.dp),
                )
            }
        },
        topBar = {
            TopAppBar(
                title = { Text(text = &quot;영화 목록&quot;) },
                navigationIcon = {
                    IconButton(onClick = { mainNavController.popBackStack() }) {
                        Icon(imageVector = Icons.Default.ArrowBack, contentDescription = null)
                    }
                }
            )
        }
    ) { paddingValues -&amp;gt;
        LazyColumn(
            Modifier.padding(paddingValues),
            userScrollEnabled = true,
        ) {
            items(movies.itemCount) { index -&amp;gt;
                val movie = movies[index]
                MovieItem(movie!!, ImageSize.getImageFixedSize())
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
private fun MovieItem(
    movie: MovieEntity,
    imageSize: ImageSize,
    onMovieClick: (movieId: Int) -&amp;gt; Unit = {}
) {
    SubcomposeAsyncImage(
        model = movie.image,
        loading = { MovieItemPlaceholder() },
        error = { MovieItemPlaceholder() },
        contentDescription = null,
        contentScale = ContentScale.Crop,
        modifier = Modifier
            .padding(3.dp)
            .size(imageSize.width, imageSize.height)
            .clickable { onMovieClick(movie.id) }
            .clip(RoundedCornerShape(2))
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;2. PagingData, LazyColumn 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 안드로이드 Jetpack Compose Paging3 라이브러리부터 불러온 데이터를 LazyColumn을 사용해서 무한 스크롤로 보여주는 UI를 구현했습니다. &lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;Paging3 라이브러리를 사용해서 Jetpack Compose로 무한 스크롤을 구현할 때는 Xml의 RecyclerView로 구현할 때 보다 코드량이 엄청 줄어들게 됩니다.&lt;/span&gt;&lt;/b&gt; Paging3 라이브러리의 메커니즘만 이해하고 있으면 비교적 간단하게 구현되는 것 같습니다.&lt;br /&gt;&lt;br /&gt;다음 포스팅에서는 LazyVerticalGrid를 사용해서 UI를 좀 더 이쁘게 꾸며보고, 영화를 클릭했을 때 클린 아키텍처 시리즈에서 구현한 영화 상세 화면으로 이동하는 기능을 구현하겠습니다.&lt;/p&gt;</description>
      <category>안드로이드/Compose Paging3</category>
      <category>Jetpack Compose</category>
      <category>pager</category>
      <category>paging3</category>
      <category>안드로이드</category>
      <author>코딩덕</author>
      <guid isPermaLink="true">https://code-tales.tistory.com/19</guid>
      <comments>https://code-tales.tistory.com/19#entry19comment</comments>
      <pubDate>Thu, 22 Feb 2024 11:30:57 +0900</pubDate>
    </item>
  </channel>
</rss>