<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>무식스택</title>
    <link>https://dev-soyeon.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Mon, 6 Jul 2026 20:37:18 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>이도일</managingEditor>
    <image>
      <title>무식스택</title>
      <url>https://tistory1.daumcdn.net/tistory/5121662/attach/7184dcfd8f654943bdc250a0ef36ea0f</url>
      <link>https://dev-soyeon.tistory.com</link>
    </image>
    <item>
      <title>android 개발팀에서 husky로 git hook 공유하기</title>
      <link>https://dev-soyeon.tistory.com/105</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;사건의 발단&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 우리팀은 commit-msg title에 prefix로 브랜치 구분(feature / chore / hotfix&amp;hellip;etc) 을 적용하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;commit-msg content에 티켓 넘버를 자동으로 붙이는 hook을 적용중이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hook은 기본적으로 공유가 안 되어서, 굳이굳이 ignore를 풀고..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브랜치를 변경할 때마다 복잡한 cli 명령어를 쓰고&amp;hellip;(매번 까먹어서 확인해야한다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;암튼 이런 짓을 해야하는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;husky 라는 node 모듈을 쓰면 hook의 공유가 가능하다는 것을 알게되었다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게다가 앞으로 추가될(지 모를) 노드 모듈들과도&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;npm install&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령어 하나만으로 묶일 수 있다는 것!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 웹 개발자들과 다르게.. 안드로이드는 노드 모듈을 사용할 일이 많지않아서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm install 하는것도 초반엔 매번 까먹겠지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어쨌든 도입해보기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;설정 방법&lt;/b&gt;&lt;/h3&gt;
&lt;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;pre class=&quot;1c&quot;&gt;&lt;code&gt;// Using npm
npx husky-init &amp;amp;&amp;amp; npm install

// Using yarn
yarn dlx husky-init --yarn2 &amp;amp;&amp;amp; yarn

// Using pnpm
pnpm dlx husky-init &amp;amp;&amp;amp; pnpm install&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;루트 프로젝트에 .husky 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled.png&quot; data-origin-width=&quot;197&quot; data-origin-height=&quot;150&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KFWGm/btsGMKai2RA/IPCkpQZxInKeTTax4QDrzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KFWGm/btsGMKai2RA/IPCkpQZxInKeTTax4QDrzK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KFWGm/btsGMKai2RA/IPCkpQZxInKeTTax4QDrzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKFWGm%2FbtsGMKai2RA%2FIPCkpQZxInKeTTax4QDrzK%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;197&quot; height=&quot;150&quot; data-filename=&quot;Untitled.png&quot; data-origin-width=&quot;197&quot; data-origin-height=&quot;150&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용할 hook 파일 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled 1.png&quot; data-origin-width=&quot;222&quot; data-origin-height=&quot;147&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Cy6oL/btsGLv58mIY/jotIBsQK0jf6U9bbYbp4h1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Cy6oL/btsGLv58mIY/jotIBsQK0jf6U9bbYbp4h1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Cy6oL/btsGLv58mIY/jotIBsQK0jf6U9bbYbp4h1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCy6oL%2FbtsGLv58mIY%2FjotIBsQK0jf6U9bbYbp4h1%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;222&quot; height=&quot;147&quot; data-filename=&quot;Untitled 1.png&quot; data-origin-width=&quot;222&quot; data-origin-height=&quot;147&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;나는 두 개를 사용한다. (pre-commit은 자동으로 그냥 생성된다!)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;commit-msg : 커밋 메세지 내용으로 티켓 넘버를 붙이기 위함&lt;/li&gt;
&lt;li&gt;prepare-commit-msg : 커밋 메세지 타이틀에 구분을(feature / hotfix&amp;hellip;) 를 붙이기 위함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;hook file 내용 작성&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# commit-msg

#!/bin/bash

COMMIT_MESSAGE_FILE_PATH=$1
MESSAGE=$(cat &quot;$COMMIT_MESSAGE_FILE_PATH&quot;)

# 커밋 메시지가 있을 때만 티켓 넘버를 추가한다.
if [[ $(head -1 &quot;$COMMIT_MESSAGE_FILE_PATH&quot;) == '' ]]; then
  exit 0
fi

# 브랜치 이름에서 마지막 '_' 이후의 문자열만 남긴다. '/'가 없다면 브랜치 전체 이름이 POSTFIX 된다.
# POSTFIX의 첫 번째 '-' 앞뒤의 문자열만 포함한다. '-'가 없다면 변경은 없다

POSTFIX=$(git branch | grep '\\*' | sed 's/* //' | sed 's/^.*\\///' | sed 's/^.*_//')

printf &quot;%s\\n\\n[%s]&quot; &quot;$MESSAGE&quot; &quot;$POSTFIX&quot; &amp;gt; &quot;$COMMIT_MESSAGE_FILE_PATH&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# prepare-commit-msg

#!/bin/sh

COMMIT_MESSAGE_FILE_PATH=$1

# merge commit에 대해서는 prefix를 생성하지 않는다.
MERGE=$(grep -c -i 'merge' &amp;lt; &quot;$COMMIT_MESSAGE_FILE_PATH&quot; || true)
if [ &quot;$MERGE&quot; != &quot;0&quot; ] ; then
  exit 0
fi

TYPE=$(git branch | grep '\\*' | sed 's/\\* //' | sed 's/\\([^/]*\\).*/\\1/' || true)
DESCRIPTION=$(git config branch.&quot;$TYPE&quot;.description || true)

echo &quot;$TYPE: $(cat &quot;$COMMIT_MESSAGE_FILE_PATH&quot;)&quot; &amp;gt; &quot;$COMMIT_MESSAGE_FILE_PATH&quot;
if [ -n &quot;$DESCRIPTION&quot; ]
then
   echo &quot;&quot; &amp;gt;&amp;gt; &quot;$COMMIT_MESSAGE_FILE_PATH&quot;
   echo &quot;$DESCRIPTION&quot; &amp;gt;&amp;gt; &quot;$COMMIT_MESSAGE_FILE_PATH&quot;
fi&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 참고로 branch naming convention은 &lt;span data-token-index=&quot;1&quot;&gt;구분/티켓명_티켓넘버&lt;/span&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;package.json에 권한 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;devDependencies&quot;: {
    &quot;husky&quot;: &quot;^9.0.11&quot;
  },
  &quot;scripts&quot;: {
    &quot;prepare&quot;: &quot;husky&quot;,
    &quot;postinstall&quot;: &quot;chmod +x .husky/&quot;
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;커밋 메세지 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled 2.png&quot; data-origin-width=&quot;569&quot; data-origin-height=&quot;137&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqkvVs/btsGMLtzizo/MYVr6XNH5BJUkfViIpJJY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqkvVs/btsGMLtzizo/MYVr6XNH5BJUkfViIpJJY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqkvVs/btsGMLtzizo/MYVr6XNH5BJUkfViIpJJY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqkvVs%2FbtsGMLtzizo%2FMYVr6XNH5BJUkfViIpJJY1%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;569&quot; height=&quot;137&quot; data-filename=&quot;Untitled 2.png&quot; data-origin-width=&quot;569&quot; data-origin-height=&quot;137&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;트러블 슈팅&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;파일 위치 문제&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;원래는 .husky 밑에 바로 hook 파일들을 넣어야한다..&lt;/span&gt;그걸 몰라서 .husky/_/하위 hook 파일의 내용을 고쳤다..... (git hook 설정시 내용 변경후 .sample확장자를 제거하면 되는것처럼)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled 3.png&quot; data-origin-width=&quot;269&quot; data-origin-height=&quot;504&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Fd75z/btsGLhNMsxP/jDUZg2SBLk2VqErVEdFFok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Fd75z/btsGLhNMsxP/jDUZg2SBLk2VqErVEdFFok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Fd75z/btsGLhNMsxP/jDUZg2SBLk2VqErVEdFFok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFd75z%2FbtsGLhNMsxP%2FjDUZg2SBLk2VqErVEdFFok%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;269&quot; height=&quot;504&quot; data-filename=&quot;Untitled 3.png&quot; data-origin-width=&quot;269&quot; data-origin-height=&quot;504&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연히 저렇게 하면 안 된다. npm install시 저 파일이 덮어씌워진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.husky/_/ 하위 파일들은 .husky/ 하위 파일의 트리거일 뿐이다&amp;hellip;..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 몰라서 한참을 헤멨다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;파일 권한 문제&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;훅 초기 세팅시에 권한이 없으면 커밋 메세지를 마음대로 커스텀 할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서&amp;hellip; npm install시에 .husky/하위 파일의 권한을 올려주는 방식으로 해결했다&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;package.json에 권한 설정 &lt;/b&gt;부분이 그 결과물이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;prepare-commit-msg hook 에러&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;husky - prepare-commit-msg script failed (code 1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 에러가 발생했다&amp;hellip;..시부레&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 전 스크립트는 다음과 같다&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;#!/bin/sh

COMMIT_MESSAGE_FILE_PATH=$1

# merge commit에 대해서는 prefix를 생성하지 않는다.
MERGE=$(grep -c -i 'merge' &amp;lt; &quot;$COMMIT_MESSAGE_FILE_PATH&quot;)
if [ &quot;$MERGE&quot; != &quot;0&quot; ] ; then
  exit 0
fi

TYPE=$(git branch | grep '\\*' | sed 's/\\* //' | sed 's/\\([^/]*\\).*/\\1/')
DESCRIPTION=$(git config branch.&quot;$TYPE&quot;.description)
echo &quot;$TYPE: $(cat &quot;$COMMIT_MESSAGE_FILE_PATH&quot;)&quot; &amp;gt; &quot;$COMMIT_MESSAGE_FILE_PATH&quot;
if [ -n &quot;$DESCRIPTION&quot; ]
then
   echo &quot;&quot; &amp;gt;&amp;gt; &quot;$COMMIT_MESSAGE_FILE_PATH&quot;
   echo &quot;$DESCRIPTION&quot; &amp;gt;&amp;gt; &quot;$COMMIT_MESSAGE_FILE_PATH&quot;
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서브 커맨드의 종료 조건이 (ex: grep -c -i 'merge' &amp;lt; .git/COMMIT_EDITMSG) 의 리턴값이 에러로 잡혀서..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;허스키는 이 경우 아예 exit을 시켜버리기 때문에 발생한 문제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 후 스크립트&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;#!/bin/sh

COMMIT_MESSAGE_FILE_PATH=$1

# merge commit에 대해서는 prefix를 생성하지 않는다.
MERGE=$(grep -c -i 'merge' &amp;lt; &quot;$COMMIT_MESSAGE_FILE_PATH&quot; || true)
if [ &quot;$MERGE&quot; != &quot;0&quot; ] ; then
  exit 0
fi

TYPE=$(git branch | grep '\\*' | sed 's/\\* //' | sed 's/\\([^/]*\\).*/\\1/' || true)
DESCRIPTION=$(git config branch.&quot;$TYPE&quot;.description || true)

echo &quot;$TYPE: $(cat &quot;$COMMIT_MESSAGE_FILE_PATH&quot;)&quot; &amp;gt; &quot;$COMMIT_MESSAGE_FILE_PATH&quot;
if [ -n &quot;$DESCRIPTION&quot; ]
then
   echo &quot;&quot; &amp;gt;&amp;gt; &quot;$COMMIT_MESSAGE_FILE_PATH&quot;
   echo &quot;$DESCRIPTION&quot; &amp;gt;&amp;gt; &quot;$COMMIT_MESSAGE_FILE_PATH&quot;
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서브 커맨드에서 true를 반환하도록 수정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 에러 때문에 진짜 한 3시간은 쓴 것 같은데ㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무튼 해결.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이왕 node module을 쓰게 된 거..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다양한 프로젝트 세팅에 필요한 모듈들을 찾아서 적용해봐야겠다.&lt;/p&gt;</description>
      <category>개발/꿀팁</category>
      <category>android</category>
      <category>githook</category>
      <category>Husky</category>
      <category>NPM</category>
      <category>안드로이드</category>
      <category>자동화</category>
      <category>허스키</category>
      <author>이도일</author>
      <guid isPermaLink="true">https://dev-soyeon.tistory.com/105</guid>
      <comments>https://dev-soyeon.tistory.com/105#entry105comment</comments>
      <pubDate>Fri, 19 Apr 2024 15:10:11 +0900</pubDate>
    </item>
    <item>
      <title>Flutter padding 위젯 util화</title>
      <link>https://dev-soyeon.tistory.com/104</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 피그마 value 값을 토큰화 시키는 작업을 진행중이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(참고로 style dictionary 는 아래와 같은 토큰값을 지원한다)&lt;/p&gt;
&lt;pre id=&quot;code_1712966956815&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;color
dimension
custom-radius
custom-spacing
custom-fontStyle
custom-icon&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/aloisdeniel/style-dictionary-figma-flutter&quot;&gt;https://github.com/aloisdeniel/style-dictionary-figma-flutter&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1712967959527&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 - aloisdeniel/style-dictionary-figma-flutter: Custom style-dictionary transforms and formats to generate Flutter resource&quot; data-og-description=&quot;Custom style-dictionary transforms and formats to generate Flutter resources from a Figma Design Token plugin export.. - aloisdeniel/style-dictionary-figma-flutter&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/aloisdeniel/style-dictionary-figma-flutter&quot; data-og-url=&quot;https://github.com/aloisdeniel/style-dictionary-figma-flutter&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bggocf/hyVMNqTdqR/H0mwSqLOMiHMcUF0duCZjk/img.png?width=1200&amp;amp;height=600&amp;amp;face=973_127_1051_211&quot;&gt;&lt;a href=&quot;https://github.com/aloisdeniel/style-dictionary-figma-flutter&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/aloisdeniel/style-dictionary-figma-flutter&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bggocf/hyVMNqTdqR/H0mwSqLOMiHMcUF0duCZjk/img.png?width=1200&amp;amp;height=600&amp;amp;face=973_127_1051_211');&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 - aloisdeniel/style-dictionary-figma-flutter: Custom style-dictionary transforms and formats to generate Flutter resource&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Custom style-dictionary transforms and formats to generate Flutter resources from a Figma Design Token plugin export.. - aloisdeniel/style-dictionary-figma-flutter&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무튼&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;code generator를 사용해서 만들어지는 값들은(ex: style dictionary) 영 마음에 안들어서...&lt;/p&gt;
&lt;pre id=&quot;code_1712967238650&quot; class=&quot;abnf&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;  final Color colorsMultipleFills0;
  final Color colorsMultipleFills1;
  final Color colorsRefBlue;
  final Color colorsSingleBlue;
  final Color colorsSpecialCharacters;
  final Color colorsSpecialCharactersNderung;
  final Color darkBackground;
  final Color invalidEmpty;
  final Color lightBackground;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런식으로 이름 그대로 지정이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;color는 material color 사용하는것처럼 color를 만들고, (이건 다음 글로)&lt;/p&gt;
&lt;pre id=&quot;code_1712967293167&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;static const dsRed = MaterialColor(_dsRedPrimaryValue, &amp;lt;int, Color&amp;gt;{
    100: Color(0xFFFFD6DA),
    200: Color(0xFFFFA3AC),
    300: Color(0xFFFF808B),
    400: Color(0xFFFF5262),
    500: Color(_dsRedPrimaryValue),
    600: Color(0xFFCC0013),
    700: Color(0xFF99000E),
    800: Color(0xFF66000A),
    900: Color(0xFF330005),
  });
  static const _dsRedPrimaryValue = 0xFFFF0018;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(이렇게 사용하면 dsRed[100]과 같은 형태로 사용이 가능하다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;padding widget은 side flag를 넣어서 아래와 같이 방향, 픽셀값만 지정하면 사용할 수 있게끔 util화 시켰다.&lt;/p&gt;
&lt;pre id=&quot;code_1712967073644&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import 'package:flutter/material.dart';

class Dimension {
  late _DimensionSide _left;
  late _DimensionSide _top;
  late _DimensionSide _right;
  late _DimensionSide _bottom;
  late _DimensionSide _horizontal;
  late _DimensionSide _vertical;
  late _DimensionSide _all;

  Dimension() {
    _left = _DimensionSide(_SideFlag(1, 0, 0, 0));
    _top = _DimensionSide(_SideFlag(0, 1, 0, 0));
    _right = _DimensionSide(_SideFlag(0, 0, 1, 0));
    _bottom = _DimensionSide(_SideFlag(0, 0, 0, 1));
    _horizontal = _DimensionSide(_SideFlag(1, 0, 1, 0));
    _vertical = _DimensionSide(_SideFlag(0, 1, 0, 1));
    _all = _DimensionSide(_SideFlag(1, 1, 1, 1));
  }

  _DimensionSide get left =&amp;gt; _left;
  _DimensionSide get top =&amp;gt; _top;
  _DimensionSide get right =&amp;gt; _right;
  _DimensionSide get bottom =&amp;gt; _bottom;
  _DimensionSide get horizontal =&amp;gt; _horizontal;
  _DimensionSide get vertical =&amp;gt; _vertical;
  _DimensionSide get all =&amp;gt; _all;
}

class _DimensionSide {
  _SideFlag sideFlag;

  _DimensionSide(this.sideFlag);

  EdgeInsets get none {
    return _DimensionSize(sideFlag, _Helper.none).data;
  }

  EdgeInsets get px_4 {
    return _DimensionSize(sideFlag, _Helper.px_4).data;
  }

  EdgeInsets get px_6 {
    return _DimensionSize(sideFlag, _Helper.px_6).data;
  }

  EdgeInsets get px_8 {
    return _DimensionSize(sideFlag, _Helper.px_8).data;
  }

  EdgeInsets get px_12 {
    return _DimensionSize(sideFlag, _Helper.px_12).data;
  }

  EdgeInsets get px_16 {
    return _DimensionSize(sideFlag, _Helper.px_16).data;
  }

  EdgeInsets get px_24 {
    return _DimensionSize(sideFlag, _Helper.px_24).data;
  }
}

class _DimensionSize {
  _SideFlag sideFlag;
  double factor;

  _DimensionSize(this.sideFlag, this.factor);

  get data =&amp;gt; EdgeInsets.fromLTRB(
        sideFlag.leftFlag * factor,
        sideFlag.topFlag * factor,
        sideFlag.rightFlag * factor,
        sideFlag.bottomFlag * factor,
      );
}

class _SideFlag {
  double leftFlag = 0;
  double topFlag = 0;
  double rightFlag = 0;
  double bottomFlag = 0;

  _SideFlag(
    this.leftFlag,
    this.topFlag,
    this.rightFlag,
    this.bottomFlag,
  );
}

class _Helper {
  static double none = 0;
  static double px_4 = 4;
  static double px_6 = 6;
  static double px_8 = 8;
  static double px_12 = 12;
  static double px_16 = 16;
  static double px_24 = 24;
}

Dimension spacer = Dimension();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 보면 알겠지만, sideflag는 boxshadow등등, 방향 지정이 필요한 곳에서 반복적으로 사용될것이므로 private로 선언했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;helper 키워드 같은 경우에는, 피그마에서 토큰 값이 추가될 경우 변경되는 부분이다. 현재 우리는 저 7가지 값만을 사용중이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전역으로 사용 가능한 객체인 spacer를 사용하면&lt;/p&gt;
&lt;pre id=&quot;code_1712967615976&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class DSDotTag extends StatelessWidget {
  final String label;
  final Color? backgroundColor;
  final Color? textColor;
  final TextStyle textStyle;

  const DSDotTag({
    super.key,
    required this.label,
    required this.backgroundColor,
    required this.textColor,
    required this.textStyle,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 0,
      color: backgroundColor,
      shape: RoundedRectangleBorder(
        borderRadius: borderRadiusWidget.px_6,
      ),
      child: Padding(
        padding: spacer.top.px_4 + spacer.bottom.px_4 + spacer.left.px_8 + spacer.right.px_6,
        child: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(
              size: 4,
              Icons.circle,
              color: textColor,
            ),
            sizedBox.horizontal.px_4,
            Text(
              label,
              style: textStyle.copyWith(color: textColor),
            ),
          ],
        ),
      ),
    );
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예시처럼&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요런 식으로..&lt;/p&gt;
&lt;pre id=&quot;code_1712967670611&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;        padding: spacer.top.px_4 + spacer.bottom.px_4 + spacer.left.px_8 + spacer.right.px_6,&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spacer를 불러와 어느 위젯에서든 자유자재로 지정이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사 디자인 시스템의 uikit를 flutter로 구현하면서,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구체적인 하드코딩 값을 사용하지 않는 위젯 코드를 끊임없이 연구중이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로도 자주 구체적인 사례를 공유하러 와야지..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;끗!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <category>Flutter</category>
      <category>Flutter Padding</category>
      <category>style dictionary</category>
      <category>디자인 시스템</category>
      <category>플러터 디자인 시스템</category>
      <author>이도일</author>
      <guid isPermaLink="true">https://dev-soyeon.tistory.com/104</guid>
      <comments>https://dev-soyeon.tistory.com/104#entry104comment</comments>
      <pubDate>Sat, 13 Apr 2024 09:26:31 +0900</pubDate>
    </item>
    <item>
      <title>Retrofit API Test</title>
      <link>https://dev-soyeon.tistory.com/103</link>
      <description>&lt;body&gt;&lt;html&gt;&lt;head&gt;&lt;link href=&quot;//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.2/styles/atom-one-dark.min.css&quot; rel=&quot;stylesheet&quot;/&gt;
&lt;script src=&quot;//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.2/highlight.min.js&quot;&gt;&lt;/script&gt;
&lt;script&gt;hljs.initHighlightingOnLoad();&lt;/script&gt;&lt;/head&gt;&lt;/html&gt;&lt;link href=&quot;https://rawcdn.githack.com/ppuep94/n2t/5ef4dc01e9d6336341e9ab95bb71672f9d3a3dc9/assets/css/style2.css&quot; rel=&quot;stylesheet&quot;/&gt;&lt;article class=&quot;page sans Notion&quot; id=&quot;97112dab-d6b0-4fd2-a523-3987b470c1c7&quot;&gt;&lt;header&gt;&lt;p class=&quot;page-description&quot;&gt;&lt;/p&gt;&lt;/header&gt;&lt;div class=&quot;page-body Tistory&quot;&gt;&lt;p class=&quot;&quot; id=&quot;9a4d6b6c-f119-45c3-aa53-126905867f81&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;6e90711a-19cb-48e6-ac3a-0d746ad1a382&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;9ae863ba-3bf4-478f-b05f-6fdc14f62cf8&quot;&gt;대부분 restful API를 사용하는 안드로이드 네이티브 개발자들이라면&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;73c19217-6954-4f1c-8d80-355143a029fd&quot;&gt;Retrofit을 쉽게 사용할것이다.&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;4e234f36-dc1a-491b-b198-d21156790200&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;05dc01a2-1043-40f2-a0bd-4b02e2c55e16&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;f0fb29d3-16ab-4486-813e-a8b1b24a8ea6&quot;&gt;구현 자체는 어렵지 않으나… API 개발은 프로젝트에서 독립적인 영역이고,&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;6d933202-278c-4e7d-8829-aebe41bf1af2&quot;&gt;때문에 시간이 지남에 따라 API의 변경이라든지, 구조의 변경..뭐 이런게 있을 수 있다.&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;61521f83-6207-4ed5-89db-b9d95eb91596&quot;&gt; 변경 정도면 양반이고….API가 없는 상태에서 개발을 시작하게 되는 경우도 드물지 않다……&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;50d8dd4c-424e-471d-935d-9681ee7482b4&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;c3424fa3-202b-4d0d-aa12-d1702630faf5&quot;&gt;이 말은 즉슨, mock test가 필수적이라는것&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;6111db99-052d-4649-beb5-9b185126fcb6&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;3ce269d6-2a68-40ae-a9f7-fdd3d785e81b&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;e39bb183-0ff4-45a1-95c1-e04c1f8fecd3&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;25d6c0de-35ad-450b-be8b-5a848a89ee32&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;54616879-825c-4eeb-8c52-4db2c392aa87&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;2026980f-b541-4e16-80da-632ab9be6ecd&quot;&gt;
&lt;/p&gt;&lt;h2 class=&quot;&quot; id=&quot;9d0278db-6697-4ecf-9b0a-a8548fac9928&quot;&gt;&lt;a href=&quot;https://github.com/square/okhttp/tree/master/mockwebserver&quot;&gt;MockWebServer&lt;/a&gt;&lt;/h2&gt;&lt;p class=&quot;&quot; id=&quot;576abdf6-d173-470f-9a5d-cbb670df3288&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;8555bf28-28b5-495a-aba6-ac85e4ac3e81&quot;&gt;&lt;strong&gt;전제 조건&lt;/strong&gt;&lt;/p&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;3694458a-a870-4aa9-8a93-a3e2f99b8bf4&quot; start=&quot;1&quot; type=&quot;1&quot;&gt;&lt;li&gt;Retrofit 기반이므로 미리 준비되어 있어야 한다&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;e809ae03-b503-4c0c-9355-1a41c5f65ebf&quot; start=&quot;2&quot; type=&quot;1&quot;&gt;&lt;li&gt;req /response 객체 있어야 한다&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;6f03f69b-d70e-4aee-88c3-4d2e04870680&quot; start=&quot;3&quot; type=&quot;1&quot;&gt;&lt;li&gt;단위 테스트의 기본을 이해해야 한다&lt;/li&gt;&lt;/ol&gt;&lt;p class=&quot;&quot; id=&quot;50798aef-1199-4765-83f6-1fe649e28984&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;cd37a547-2958-4b92-b74f-e7a131bf8afe&quot;&gt;&lt;strong&gt;구현 방법&lt;/strong&gt;&lt;/p&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;56670b84-500a-476d-8f7c-fad09d232a55&quot; start=&quot;1&quot; type=&quot;1&quot;&gt;&lt;li&gt;dependency 추가&lt;/li&gt;&lt;/ol&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;5aa04e6d-c7ff-4bcf-b356-85278c42c57b&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;testImplementation (&quot;com.squareup.okhttp3:mockwebserver: 4.11 . 0 &quot;)&lt;/code&gt;&lt;/pre&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;2d504532-97f1-42cd-97c7-c9e93bfcf521&quot; start=&quot;2&quot; type=&quot;1&quot;&gt;&lt;li&gt;API interface 준비&lt;/li&gt;&lt;/ol&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;48e67358-57a2-42fe-b99a-fac0a9632e74&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;interface FeedbackApi {

    @GET(&quot;feedback/user/get/?&quot;)
    suspend fun getFeedbackQuestions(
        @Query(&quot;uid&quot;) userId: String,
        @Query(&quot;fid&quot;) featureId: String
    ): FeedbackQuesDTO
}&lt;/code&gt;&lt;/pre&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;f857f19c-222e-40a8-9c74-5ee28fcd2135&quot; start=&quot;3&quot; type=&quot;1&quot;&gt;&lt;li&gt;API에 해당하는 테스트 클래스 만들기 &lt;ol class=&quot;numbered-list&quot; id=&quot;fd8b89d2-f97a-4502-85f2-69b71db2bbef&quot; start=&quot;1&quot; type=&quot;a&quot;&gt;&lt;li&gt;tip ; &lt;em&gt;이 인터페이스를 클래스로 변경한 다음 ALT+INSERT -&amp;gt; 테스트 -&amp;gt; 테스트 클래스 생성을 누른 다음 다시 인터페이스로 되돌리면 됨&lt;/em&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;853d8da8-eecf-4505-b82a-788e5d1d9ac6&quot; start=&quot;4&quot; type=&quot;1&quot;&gt;&lt;li&gt;테스트 클래스에 mock server의 인스턴스, 인터페이스 준비&lt;/li&gt;&lt;/ol&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;43118743-a9c9-492c-a20f-05c2e08aeb4a&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;class  FeedbackApiTest { 
    private  lateinit  var server: MockWebServer 
    private  lateinit  var api: FeedbackApi 

    @BeforeEach //JUnit5 사용 
    fun  beforeEach () { 
        server = MockWebServer() 
        api = Retrofit.Builder() 
            .baseUrl(server. url( &quot;/&quot; )) //이와 같은 기본 URL을 전달
             . .addConverterFactory(GsonConverterFactory.create()) 
            .build().create(FeedbackApi:: class .java) 
    } 
}&lt;/code&gt;&lt;/pre&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;c3885784-e6c4-4a86-ad05-42a48655c148&quot; start=&quot;5&quot; type=&quot;1&quot;&gt;&lt;li&gt;memory leak 을 방지하기 위해 서버 종료도 잊지말기&lt;/li&gt;&lt;/ol&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;bd9129d9-bcda-49dc-a900-a643bccec526&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;@AfterEach 
    fun  afterEach () { 
        server.shutdown() 
    }&lt;/code&gt;&lt;/pre&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;1befe909-72fd-4abe-95eb-1737b47ab018&quot; start=&quot;6&quot; type=&quot;1&quot;&gt;&lt;li&gt;테스트 코드 예시&lt;/li&gt;&lt;/ol&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;469e605b-ed02-4e6b-82a5-19ce8bfd3f29&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;@Test 
    fun `getFeedbackQuestions, presents Success` () = runTest { 
        val dto = FeedbackQuesDTO() //응답으로 돌려주고 싶은 객체 
        private  val gson: Gson = GsonBuilder().create() 
        val json = gson.toJson(dto )!! //GSON을 사용하여 객체를 json 문자열로 변환 
        val res = MockResponse() //서버 호출
         res.setBody(json) //fake response의 body를 json string으로 변환
         server.enqueue(res) //서버 응답 큐에 추가 

        val  data = api.getFeedbackQuestions( &quot;&quot; , &quot;&quot; ) // fake call
         server.takeRequest () //서버가 요청을 받도록

         .assertEquals( data , dto) //호출 응답으로 얻는 데이터는 동일해야함
     . }&lt;/code&gt;&lt;/pre&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;71ef04bb-b801-4aed-8ca4-9bfe7552ca4a&quot; start=&quot;7&quot; type=&quot;1&quot;&gt;&lt;li&gt;wrapper사용하는 경우&lt;/li&gt;&lt;/ol&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;fd75c585-83ef-4193-a61d-0e5bd87e3b8e&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;@Test 
    fun `getFeedbackQuestions, presents Success` () = runTest { 
        val res = MockResponse() //서버 호출에 대한 가짜 응답 만들기
         res.setBody( &quot;[]&quot; ) // 응답으로 예상되는 json 문자열
         server.enqueue(res) //서버 응답 큐에 추가 

        val  data = api.getFeedbackQuestions( &quot;&quot; , &quot;&quot; ) //가짜 서버를 호출
         server.takeRequest() //서버가 요청을 받도록

         AssertEquals( data .body()!!.isEmpty(), true ) //호출 응답으로 얻는 데이터는 동일해야함
     }&lt;/code&gt;&lt;/pre&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;6dc4442e-8c70-49f5-b836-6b6e8afdaf5f&quot; start=&quot;8&quot; type=&quot;1&quot;&gt;&lt;li&gt;응답코드 설정해서 test하는 경우&lt;/li&gt;&lt;/ol&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;6eac549b-a1dd-4364-811d-9b3af7ae8c44&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;fun `getFeedbackQuestions, returns Error`() = runTest {
        //서버가 응답을 받을 수 있도록 준비
        val res = MockResponse()
        res.setResponseCode(404)
        server.enqueue(res)

        //호출 생성
        val repo = FeedbackRepoImpl(api, mockk())
        val data = repo.getFeedbackQuestions(&quot;&quot;, &quot;&quot;)

        //서버에 생성된 호출을 수락하도록 지시
        server.takeRequest()

        assert(data is MyResponseState.Error)//응답코드 400이므로 RepoImpl을 사용하여 MyResponseState.Error를 보냄
    }&lt;/code&gt;&lt;/pre&gt;&lt;html&gt;&lt;body&gt;&lt;br/&gt;&lt;p class=&quot;&quot;&gt;Uploaded by &lt;mark class=&quot;highlight-orange&quot;&gt;&lt;a href=&quot;https://github.com/jmjeon94/N2T&quot;&gt;N2T&lt;/a&gt;&lt;/mark&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;&lt;/div&gt;&lt;/article&gt;&lt;span class=&quot;sans&quot; style=&quot;font-size:14px;padding-top:2em&quot;&gt;&lt;/span&gt;&lt;/body&gt;</description>
      <category>개발/Android</category>
      <category>android</category>
      <category>Kotlin</category>
      <category>MockWebServer</category>
      <category>Network</category>
      <category>retrofit</category>
      <category>네트워크</category>
      <category>안드로이드</category>
      <category>코틀린</category>
      <author>이도일</author>
      <guid isPermaLink="true">https://dev-soyeon.tistory.com/103</guid>
      <comments>https://dev-soyeon.tistory.com/103#entry103comment</comments>
      <pubDate>Wed, 3 Jan 2024 10:55:34 +0900</pubDate>
    </item>
    <item>
      <title>메모리 누수를 해결하는 조금 더 자세한 방법</title>
      <link>https://dev-soyeon.tistory.com/102</link>
      <description>&lt;body&gt;&lt;html&gt;&lt;head&gt;&lt;link href=&quot;//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.2/styles/atom-one-dark.min.css&quot; rel=&quot;stylesheet&quot;/&gt;
&lt;script src=&quot;//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.2/highlight.min.js&quot;&gt;&lt;/script&gt;
&lt;script&gt;hljs.initHighlightingOnLoad();&lt;/script&gt;&lt;/head&gt;&lt;/html&gt;&lt;link href=&quot;https://rawcdn.githack.com/ppuep94/n2t/5ef4dc01e9d6336341e9ab95bb71672f9d3a3dc9/assets/css/style2.css&quot; rel=&quot;stylesheet&quot;/&gt;&lt;article class=&quot;page sans Notion&quot; id=&quot;a8bd4e92-0e50-488f-8948-06a3cd1b3530&quot;&gt;&lt;header&gt;&lt;p class=&quot;page-description&quot;&gt;&lt;/p&gt;&lt;/header&gt;&lt;div class=&quot;page-body Tistory&quot;&gt;&lt;p class=&quot;&quot; id=&quot;9063e488-3335-4b16-84c6-b97b5a5e747f&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;6b18a39f-2a92-4c32-b670-a42518d28ef4&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;7fef4ca1-a304-4a97-a73a-e9cec5b94616&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;4d3ed0fb-8b63-4ae3-8447-c389e097cd76&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;e2ef9bff-8512-4ff8-b680-9e1c7d10c02e&quot;&gt;이전에 관련된 글을 작성한 적 있으므로, &lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;7a3c7cb5-2056-46ff-a905-94acf19d3b09&quot;&gt;중복된 항목에 대해서 이유를 자세하게 적지는 않겠다.&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;7faa3ca5-26d7-42c7-b029-d2182d6651fa&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;46ef4624-44af-42b6-bca6-9c9b93625bf4&quot;&gt;
&lt;/p&gt;&lt;h2 class=&quot;&quot; id=&quot;0e7ad094-62c4-4f97-84c2-726e30a6fbf4&quot;&gt;Inner class와 익명 class&lt;/h2&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;5243c976-7277-447e-a533-0c08dbb1cd94&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;밖의 클래스보다 inner class의 주기가 더 길면 생기는 문제&lt;/li&gt;&lt;/ul&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;0603cff8-b27d-4602-96eb-222febe51be4&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;암시적 참조를 주의할것&lt;/li&gt;&lt;/ul&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;5dbb66c7-8019-4fbf-9402-deceecebc060&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;class MyActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_my)

        val button = findViewById&amp;lt;Button&amp;gt;(R.id.myButton)
        button.setOnClickListener {
            // This is an anonymous inner class
            // It holds an implicit reference to MyActivity
            // Long-running operations or delayed execution here can cause leaks
        }
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;75a71ae3-12ba-4b82-9d10-a91362c78a23&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;onCreate 안에서 click listener가 설정되었으므로, 저 리스너는 activity에 대해서 암시적 참조를 가지게된다.&lt;p class=&quot;&quot; id=&quot;5f2a020f-0b05-4d72-afda-38f9099ea49b&quot;&gt;→ 리스너의 행위는 정해진게 없으므로, 만약 리스너가 작업을 계속 진행하는 동안 activty가 삭제되거나 하면 메모리 누수가 발생함.&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;57b42fd2-a4a9-4e9b-a1b5-3e240f8e8b23&quot;&gt;
&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;76b4a164-1ce9-4280-9fa2-69e4d35d475e&quot;&gt;&lt;strong&gt;해결 방안&lt;/strong&gt;&lt;/p&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;e5a657e5-2792-4e75-9a8a-85240c1e61b7&quot; start=&quot;1&quot; type=&quot;1&quot;&gt;&lt;li&gt;static inner class를 사용할것&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;ee444f95-e07d-4de4-b76d-431651aee049&quot; start=&quot;2&quot; type=&quot;1&quot;&gt;&lt;li&gt;weakReference를 사용할것&lt;/li&gt;&lt;/ol&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;8ffe628c-95c3-4a70-9f18-f307671e97ce&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;class MyActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_my)

        MyStaticAsyncTask(this).execute()
    }

    private class MyStaticAsyncTask(activity: MyActivity) : AsyncTask&amp;lt;Void, Void, String&amp;gt;() {
        private val activityReference = WeakReference(activity)

        override fun doInBackground(vararg params: Void?): String {
            // Perform a long-running task
            return &quot;Result&quot;
        }

        override fun onPostExecute(result: String) {
            activityReference.get()?.let { activity -&amp;gt;
                // Safely update the Activity's UI
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;a4e2099a-ceee-4d0f-af63-8bee25e5b858&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;Activity에 대한 weakReference를 가지고 있는게 포인트!&lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;6f40129b-dab8-4041-81a6-5057c0603e0e&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;21ef42df-c9be-40bd-a4aa-16f9830422b7&quot;&gt;
&lt;/p&gt;&lt;h2 class=&quot;&quot; id=&quot;5c5d8e95-bbe4-4040-8e29-b8e0d9e7c0a5&quot;&gt;Handler &amp;amp; Runnable&lt;/h2&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;39a673cb-2a29-4b31-aa76-59ab6ab4276a&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;마찬가지로, 스레드에서 뷰에 대한 암시적인 참조가 있을 때 문제가 됨.&lt;/li&gt;&lt;/ul&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;28f5c5f8-f84b-4200-a12e-cb8abed7eda6&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;딜레이가 있는 실행동안 뷰가 삭제되면 GC안될 수 있음&lt;/li&gt;&lt;/ul&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;5cef4bc7-9bf3-4104-8050-be28df594541&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;class  MyActivity : AppCompatActivity () { 

    private  val handler = Handler() 

    private  val updateRunnable = object : Runnable { 
        override  fun  run () { 
            // 해당 Runnable은 MyActivity에 대한 암시적 참조를 보유
            // 여기서 장기 실행 작업이나 지연 실행으로 인해 Leaks
         } 
    } 

    override  fun  onCreate (savedInstanceState: Bundle ?) { 
        super .onCreate(savedInstanceState) 
        setContentView(R.layout.activity_my) 

        // 지연된 작업 게시
         handler.postDelayed(updateRunnable, 60000 ) // 60초 지연
     } 
}&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;34cb48fb-1d3b-4040-b467-8cb616eebeab&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;a587580c-3eaa-4079-8120-c8f412571b25&quot;&gt;&lt;strong&gt;해결 방안&lt;/strong&gt;&lt;/p&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;4ab91a8a-e569-4443-a8d5-8a729ba3e2a5&quot; start=&quot;1&quot; type=&quot;1&quot;&gt;&lt;li&gt;static inner class 혹은 별도의 클래스 사용할 것&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;4fd7f2c7-c337-4246-8945-a1b42c3ea617&quot; start=&quot;2&quot; type=&quot;1&quot;&gt;&lt;li&gt;onDestroy()에서 알아서 주울것&lt;/li&gt;&lt;/ol&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;45211e92-7d36-4df1-9336-6a6bb49ab34a&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;class MyActivity : AppCompatActivity() {

    private val handler = Handler()
    private val updateRunnable = UpdateRunnable(this)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_my)

        handler.postDelayed(updateRunnable, 60000) // 60-second delay
    }

    override fun onDestroy() {
        super.onDestroy()
        handler.removeCallbacks(updateRunnable) // Remove pending callbacks
    }

    private class UpdateRunnable(activity: MyActivity) : Runnable {
        private val weakActivity = WeakReference(activity)

        override fun run() {
            weakActivity.get()?.let {
                // Perform actions without risking a memory leak
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;35b0961d-d2d1-4f52-bab5-b0ee77e9c111&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;마찬가지로 weakReference를 가지고 있는게 포인트~&lt;/li&gt;&lt;/ul&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;746397d9-bcec-4aff-83f1-399a78aa5703&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;추가로, destroy 시점에 적절하게 callback을 삭제해줌&lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;989fdfc5-1266-4975-961a-75742f71997a&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;4b7b8e98-f650-4b0f-9f3a-d550ab9882ec&quot;&gt;
&lt;/p&gt;&lt;h2 class=&quot;&quot; id=&quot;78a3d9eb-4ad0-43bf-80c8-7a86bd79422d&quot;&gt;익명 listener&lt;/h2&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;00f88bdb-a6b5-4cea-8ac2-c97adc6a2f12&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;리스너가 뷰 내부에 선언되면서 암시적인 참조를 보유하게 될 때 생기는 문제&lt;/li&gt;&lt;/ul&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;a5886bc2-4d53-450b-b5e9-daffe18c2364&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;1번 예시가 곧 예제가 됨&lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;d7cc6641-ec22-4078-b86c-bd6d1febb4a3&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;3afc091e-f6fe-4d74-b019-ef0dadc88809&quot;&gt;&lt;strong&gt;해결 방안&lt;/strong&gt;&lt;/p&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;1a882cd2-f640-455f-8362-e249949d5c83&quot; start=&quot;1&quot; type=&quot;1&quot;&gt;&lt;li&gt;리스너에서 장기적으로 실행되는 작업이 없도록 방지할 것&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;92c57b54-4ee7-461a-b727-f7c568fbea09&quot; start=&quot;2&quot; type=&quot;1&quot;&gt;&lt;li&gt;마찬가지로 WeakReference를 사용할것!&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;d8cd50eb-9461-433d-9594-a56504cabb7a&quot; start=&quot;3&quot; type=&quot;1&quot;&gt;&lt;li&gt;static 중첩 클래스나 람다를 활용할 것 → 약한 참조를 가능하게 해줌!&lt;/li&gt;&lt;/ol&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;a7224120-7d7d-4320-a0ae-32b13c5293fd&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;class MyActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_my)

        val button = findViewById&amp;lt;Button&amp;gt;(R.id.myButton)
        button.setOnClickListener { view -&amp;gt;
            // Lambda expression for click listener
            // Handle click event without risking a memory leak
        }
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;48bb697e-460d-49ea-844b-b67f1ab92523&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;da8e6f50-7f49-4f84-9ea1-c853cbd41b17&quot;&gt;
&lt;/p&gt;&lt;h2 class=&quot;&quot; id=&quot;8e0f6cdf-9278-486b-88aa-bac49321a7ca&quot;&gt;static view / context&lt;/h2&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;0a503499-0f6f-4a9f-bec3-8aef0eee3c6b&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;정적인 필드가 뷰에 대한 참조를 보유할 때 발생함..&lt;/li&gt;&lt;/ul&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;da160a4d-c8ef-4ff8-a6da-8a6f14f828d6&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;컨텍스트가 사용되지 않더라도 GC가 메모리를 회수하는것을 방지하게됨&lt;/li&gt;&lt;/ul&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;53afac9d-6a95-4e5a-bd08-01cb52765c51&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;class  MyActivity : AppCompatActivity () { 

    companion  object { 
        // 뷰에 대한 정적 참조 
        var staticView: View? = null
     } 

    override  fun  onCreate (savedInstanceState: Bundle ?) { 
        super .onCreate(savedInstanceState) 
        setContentView(R.layout.activity_my) 

        staticView = findViewById(R.id.myView) // 정적 필드에 뷰 할당
     } 
}&lt;/code&gt;&lt;/pre&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;ea478857-f800-4795-9542-64c22e71571b&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;companion object는 인스턴스가 아닌 속한 클래스에 연결됨&lt;/li&gt;&lt;/ul&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;18763087-76a6-4574-9092-cd20aa14bff6&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;위의 object는 activity가 삭제 된 후에도 참조를 유지함&lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;136d2b6e-d644-41df-981c-b451ecde24be&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;cf36dd01-5412-48de-b36c-405f2965ffa7&quot;&gt;&lt;strong&gt;해결 방안&lt;/strong&gt;&lt;/p&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;8edeb2e7-75c9-43c3-859f-0afafd588795&quot; start=&quot;1&quot; type=&quot;1&quot;&gt;&lt;li&gt;static한 친구에게 view의 참조를 넘기지말것…&lt;ol class=&quot;numbered-list&quot; id=&quot;72aba84f-b31a-444d-8b84-69ecdcb89f29&quot; start=&quot;1&quot; type=&quot;a&quot;&gt;&lt;li&gt;뭔가 데이터 및 상태를 유지해야 하는 상황이라면&lt;ol class=&quot;numbered-list&quot; id=&quot;ea08f005-6bae-49e2-a18a-515ea27a50c1&quot; start=&quot;1&quot; type=&quot;i&quot;&gt;&lt;li&gt;→ viewmodel / savedInstanceState를 사용할것!&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;/ol&gt;&lt;p class=&quot;&quot; id=&quot;bdf5b17d-f5b3-415b-a4a3-02e85e3e1511&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;3b89012b-00cd-4384-aea4-8b73b304a914&quot;&gt;
&lt;/p&gt;&lt;h2 class=&quot;&quot; id=&quot;59e33f4c-1c72-4f71-ab28-23efa09cebee&quot;&gt;LiveData의 부적절한 사용&lt;/h2&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;0215b7f0-6fbe-4b8e-8c40-dfcfc4f87749&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;LiveData의 수명 주기가 소유자보다 오래 지속되는 경우 발생&lt;/li&gt;&lt;/ul&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;e1d1e11a-3396-4e0c-b832-c55527c6fb7f&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;class  MyActivity : AppCompatActivity () { 

    private  val viewModel: MyViewModel by viewModels() 

    override  fun  onCreate (savedInstanceState: Bundle ?) { 
        super .onCreate(savedInstanceState) 
        setContentView(R.layout.activity_my) 

        // 부적절한 관찰
         viewModel.myLiveData.observe( this , Observer { data -&amp;gt; 
            // UI 업데이트 - 올바르게 처리되지 않으면 메모리 누수가 발생할 수 있습니다.
         }) 
    } 
}&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;5317d200-c0e7-4627-84db-629937264982&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;3111983c-feae-40f0-80d2-5ed73b02a739&quot;&gt;&lt;strong&gt;해결 방안&lt;/strong&gt;&lt;/p&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;51514bb7-76f2-4fdc-a003-caeb30fcdefc&quot; start=&quot;1&quot; type=&quot;1&quot;&gt;&lt;li&gt;viewLifeCycleOwner 사용&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;8914466d-2ca2-4999-a1e4-b3d843aa06ed&quot; start=&quot;2&quot; type=&quot;1&quot;&gt;&lt;li&gt;destroy에서 적절하게 옵저버 제거&lt;/li&gt;&lt;/ol&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;50eac084-3d10-4608-b602-ce98b238e3f2&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;class  MyFragment : Fragment () { 

    private  val viewModel: MyViewModel by viewModels() 

    override  fun  onViewCreated (view: View , saveInstanceState: Bundle ?) { 
        super .onViewCreated(view, selectedInstanceState) 

        viewModel.myLiveData.observe(viewLifecycleOwner, Observer { data - &amp;gt; 
            // UI 업데이트 - viewLifecycleOwner로 올바르게 관찰
         }) 
    } 
}&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;4b5e0a1b-b35a-4787-b198-c9e5ea889a3e&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;824bebd9-8f57-4a4a-9545-75182738a491&quot;&gt;
&lt;/p&gt;&lt;h2 class=&quot;&quot; id=&quot;5eece510-9934-4acc-97a4-fbb3a7b655c6&quot;&gt;Context 가 있는 Singleton&lt;/h2&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;4db1d330-f165-4bda-9140-4cd3c9a93b9e&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;당연히 안됨..&lt;/li&gt;&lt;/ul&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;a5d4d3a0-288d-41d9-b64d-e8f0f3ce9379&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;singleton은 전체 생애주기 중 단 하나만 존재하도록 설계됨&lt;p class=&quot;&quot; id=&quot;bd2aea6e-002f-44bc-bbaf-ef77b0972a69&quot;&gt;→ 당연히 context가 있음 안된ㄷ ㅏ!&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;740230dc-ab68-494c-a257-4018c01224ae&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;class MySingleton private constructor(context: Context) {
    init {
        // Initialization code that uses the context
    }

    companion object {
        private var instance: MySingleton? = null

        fun getInstance(context: Context): MySingleton {
            if (instance == null) {
                instance = MySingleton(context)
            }
            return instance!!
        }
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;522eca41-ce96-4c75-9620-c8300f9ae489&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;b4f5f709-304d-4155-94e5-ef148d45cdd1&quot;&gt;&lt;strong&gt;해결방안&lt;/strong&gt;&lt;/p&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;545cc1a6-eb23-4848-86dd-da1f060f22a5&quot; start=&quot;1&quot; type=&quot;1&quot;&gt;&lt;li&gt;굳이 context가 필요하다면 application context를 사용하자&lt;/li&gt;&lt;/ol&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;4eab6229-e179-43e2-a17d-f97ac59613a9&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;class MySingleton private constructor(context: Context) {
    init {
        // Use the application context to avoid memory leaks
        val applicationContext = context.applicationContext
    }

    companion object {
        private var instance: MySingleton? = null

        fun getInstance(context: Context): MySingleton {
            if (instance == null) {
                instance = MySingleton(context.applicationContext)
            }
            return instance!!
        }
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;77666bc4-1bc4-4947-bc2f-85edcffcd0e8&quot; start=&quot;2&quot; type=&quot;1&quot;&gt;&lt;li&gt;가능하다면 그냥 쓰지말자&lt;/li&gt;&lt;/ol&gt;&lt;p class=&quot;&quot; id=&quot;9d1e8715-6884-4c07-8775-a5abd1a6c2e0&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;bbec9605-3a90-4e20-a261-02215fd82845&quot;&gt;
&lt;/p&gt;&lt;h2 class=&quot;&quot; id=&quot;54de6f11-2fe5-49d0-bc32-89cf08b610f5&quot;&gt;비트맵&lt;/h2&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;7fb0d9ce-a8c9-4e9b-916e-ece4a5877146&quot; start=&quot;1&quot; type=&quot;1&quot;&gt;&lt;li&gt;고해상도 이미지를 처리 할 때 해당 부분이 문제가 될 수 있음&lt;ul class=&quot;bulleted-list&quot; id=&quot;c55818c3-b208-4190-8ab8-196662e7b31d&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;OutOfMemoryError가 발생 할 수 있음(처리 과정 중에) &lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;24caa6ad-09b0-474f-9a54-2d4f2d8cd1bd&quot; start=&quot;2&quot; type=&quot;1&quot;&gt;&lt;li&gt;명시적으로 재활용하지 않을 경우 메모리가 해제되지 않아 문제가 있을 수 있음&lt;/li&gt;&lt;/ol&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;86a0cd7f-3f7d-46f0-b3a6-974a7dcbb703&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;class MyActivity : AppCompatActivity() {

    private var myBitmap: Bitmap? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_my)

        val imageView = findViewById&amp;lt;ImageView&amp;gt;(R.id.myImageView)
        myBitmap = BitmapFactory.decodeResource(resources, R.drawable.large_image)
        imageView.setImageBitmap(myBitmap)
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;996c237e-3f73-45b2-95cd-d7d7e18bd80e&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;72a66cab-1c8b-4904-8052-6c3c226484ca&quot;&gt;&lt;strong&gt;해결방안&lt;/strong&gt;&lt;/p&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;713fb46a-ac03-4312-a3c8-989cf4c664a1&quot; start=&quot;1&quot; type=&quot;1&quot;&gt;&lt;li&gt;적절한 사이즈의 비트맵을 사용할 것. (표시에 필요한 사이즈로만)&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;d8989774-dcc9-4f2e-9df9-f2f722bf9a8f&quot; start=&quot;2&quot; type=&quot;1&quot;&gt;&lt;li&gt;onDestroy에 recycle을 해줄것&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;48933401-2430-4b66-88cb-4a782ce4abed&quot; start=&quot;3&quot; type=&quot;1&quot;&gt;&lt;li&gt;라이브러리 사용할것 (글라이드 / 피카소 / 코일)&lt;/li&gt;&lt;/ol&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;60320fa2-48fc-4e38-8f05-756b5dbc8faf&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;class MyActivity : AppCompatActivity() {

    private var myBitmap: Bitmap? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_my)

        val imageView = findViewById&amp;lt;ImageView&amp;gt;(R.id.myImageView)
        myBitmap = BitmapFactory.decodeResource(resources, R.drawable.large_image)
        // Resize bitmap as needed before setting it to ImageView
        imageView.setImageBitmap(myBitmap)
    }

    override fun onDestroy() {
        super.onDestroy()
        myBitmap?.recycle() // Recycle the bitmap
        myBitmap = null
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;7c5a2dcd-af81-4236-bfcb-bec5534f9ac2&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;보면 알겠지만 recycle을 해주고있다~&lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;abeab300-3f72-4eea-ac00-7863d6a29946&quot;&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;&lt;h2 class=&quot;&quot; id=&quot;16231a17-dc23-4524-bcac-c5c90ae94ed6&quot;&gt;웹뷰&lt;/h2&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;8baeb41d-4a4f-48eb-b822-2f9a1441eeb0&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;앱 내에서 웹 콘텐츠를 처리하는 방식 때문에 문제가 생길 수 있다~&lt;/li&gt;&lt;/ul&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;ac1d728d-5536-4102-bafd-cf65d9de2445&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;웹뷰의 주기는 속해있는 액티비티 / 프래그먼트와 다르다. 제대로 관리되지 않으면 상위 뷰의 주기가 끝났는데도 리소스를 계속 소비할 수 있다&lt;/li&gt;&lt;/ul&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;79a1865b-e2f4-489c-ac80-21aaeb81e3ac&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;class MyActivity : AppCompatActivity() {

    private lateinit var myWebView: WebView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_my)

        myWebView = findViewById(R.id.myWebView)
        myWebView.loadUrl(&quot;https://example.com&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;9ed57db1-f4e5-4640-af81-a9dfca1cb37e&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;현재 웹뷰가 뷰의 컨텍스트를 암시적으로 참조하는 부분에서 초기화되고있음&lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;121c4874-78bf-453c-a1e0-9f098c9ae733&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;a49b4df2-f58b-44ee-bbdc-715f6d651756&quot;&gt;&lt;strong&gt;해결 방안&lt;/strong&gt;&lt;/p&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;44a3f468-6ac7-4e73-9cb4-1354ef40cf20&quot; start=&quot;1&quot; type=&quot;1&quot;&gt;&lt;li&gt;적절히 해제할것 : webview.destroy()를 뷰가 해제되는 시점에 호출할 수 있게 할것&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;3eb15377-0a02-4d78-b830-69b247d0b5cb&quot; start=&quot;2&quot; type=&quot;1&quot;&gt;&lt;li&gt;application context를 사용할 것 : 만약 컨텍스트의 참조가 필요한 상황이라면 application context를 사용할 것&lt;ol class=&quot;numbered-list&quot; id=&quot;20c47fa4-d9ef-4f23-9a3f-ed8c54dec176&quot; start=&quot;1&quot; type=&quot;a&quot;&gt;&lt;li&gt;앱 컨텍스트가 항상 나쁜것만은 아니다!&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;/ol&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;de37eb4a-2656-4685-bea3-f4ea95135f14&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;class MyActivity : AppCompatActivity() {

    private lateinit var myWebView: WebView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_my)

        myWebView = findViewById(R.id.myWebView)
        myWebView.loadUrl(&quot;https://example.com&quot;)
    }

    override fun onDestroy() {
        myWebView.destroy() // Properly destroy the WebView
        super.onDestroy()
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;9a54b479-1059-4e57-8187-98c0f048b6ac&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;e28972ff-d372-4803-b774-597886c2ddc0&quot;&gt;
&lt;/p&gt;&lt;h2 class=&quot;&quot; id=&quot;f079ad64-35b4-4dd3-a891-9b0bce3834c9&quot;&gt;BroadCast Receiver&lt;/h2&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;1ced2ef4-5023-4072-a176-a40b23eba3f0&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;얘 또한.. 참조를 보유할 수 있기때문에, 뷰가 사라지는 시점에 적절히 취소해주지 않으면 메모리에 혼백이 되어서 떠돌게된다&lt;/li&gt;&lt;/ul&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;7b2c38e3-edfa-472d-863e-66a34e22d03f&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;class  MyActivity : AppCompatActivity () { 

    private  val myReceiver = object : BroadcastReceiver() { 
        override  fun  onReceive (context: Context ?,intent: Intent ?) { 
            // 브로드캐스트 처리
         } 
    } 

    override  fun  onCreate (savedInstanceState: Bundle ?) { 
        super .onCreate(savedInstanceState) 
        setContentView(R.layout.activity_my) 

        // 수신자 등록
         IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED). also { filter -&amp;gt; 
            registerReceiver(myReceiver, filter) 
        } 
    } 
}&lt;/code&gt;&lt;/pre&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;47a5644b-a7f5-4810-90d4-c7c37ac544d2&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;해당 경우에서는 액티비티 삭제되어도 계속 참조를 보유하게 된다&lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;b6467423-b9e2-44e6-af29-246c69a2f8b0&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;dd73fe3f-4a11-4b2a-a5c3-ec74008eee3c&quot;&gt;&lt;strong&gt;해결 방안&lt;/strong&gt;&lt;/p&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;8d2a51d5-3ea1-45c5-92d6-2086ada67361&quot; start=&quot;1&quot; type=&quot;1&quot;&gt;&lt;li&gt;알아서 destroy 적절히 해주기&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;cd5678a7-cc42-4917-95a7-57d4898c53be&quot; start=&quot;2&quot; type=&quot;1&quot;&gt;&lt;li&gt;(&lt;em&gt;익명 클래스 피하기&lt;/em&gt;)&lt;/li&gt;&lt;/ol&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;3dd02d97-f9a1-4163-bcdb-fa07f50f3d1e&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;class  MyActivity : AppCompatActivity () { 

    private  val myReceiver = object : BroadcastReceiver() { 
        override  fun  onReceive (context: Context ?,intent: Intent ?) { 
            // 브로드캐스트 처리
         } 
    } 

    override  fun  onCreate (savedInstanceState: Bundle ?) { 
        super .onCreate(savedInstanceState) 
        setContentView(R.layout.activity_my) 

        IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED). also { filter -&amp;gt; 
            registerReceiver(myReceiver, filter) 
        } 
    } 

    override  fun  onDestroy () { 
        super .onDestroy() 
        unregisterReceiver(myReceiver) // 수신자 등록 취소
     } 
}&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;541048ec-648f-4b24-accd-0ed08d41a882&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;ddbfba48-95ad-49c7-9121-69d04a219112&quot;&gt;
&lt;/p&gt;&lt;h2 class=&quot;&quot; id=&quot;981a12d4-6da3-4dbd-abf8-a549d139250a&quot;&gt;RecyclerView Adapter의 event listener&lt;/h2&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;bc65e321-d29b-4874-a90f-e6da75a23463&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;어댑터나 뷰홀더가 뷰에 대한 참조를 보유할 때 발생함&lt;/li&gt;&lt;/ul&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;9838e6c7-1466-4a4e-abc5-b1b62f0d0dac&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;class MyAdapter(private val items: List&amp;lt;Item&amp;gt;, private val context: Context) : RecyclerView.Adapter&amp;lt;MyAdapter.ViewHolder&amp;gt;() {

    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val button: Button = view.findViewById(R.id.myButton)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(context).inflate(R.layout.item_view, parent, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.button.setOnClickListener {
            // This listener holds a reference to 'context' which can cause a memory leak
        }
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;02273f80-5925-469a-b0b5-3de21b90977b&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;e5ab4577-253a-49a2-9339-267b663097b8&quot;&gt;&lt;strong&gt;해결 방안&lt;/strong&gt;&lt;/p&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;7a4355e4-64d3-47a0-bcbe-420f9db29103&quot; start=&quot;1&quot; type=&quot;1&quot;&gt;&lt;li&gt;어댑터에 컨텍스트 전달하지말것.&lt;ol class=&quot;numbered-list&quot; id=&quot;3c44ac55-2d45-4b96-80c9-fa1294ba879b&quot; start=&quot;1&quot; type=&quot;a&quot;&gt;&lt;li&gt;parent.context&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;3bba8232-2433-472a-8254-8f43ee43ab1f&quot; start=&quot;2&quot; type=&quot;a&quot;&gt;&lt;li&gt;onCreateView에서는 holder.itemview.context를 사용하세요~&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;344d5a78-d9a8-47bb-8e97-bae8dbbdc359&quot; start=&quot;2&quot; type=&quot;1&quot;&gt;&lt;li&gt;리스너에 weakReference를 사용하세용&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;7f36c6d5-0464-40a3-afcb-3b906b53aee4&quot; start=&quot;3&quot; type=&quot;1&quot;&gt;&lt;li&gt;리스너를 잘 분리하세용&lt;/li&gt;&lt;/ol&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;7c4e34b6-160b-426b-bfbd-ce48a53be33d&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;class MyAdapter(private val items: List&amp;lt;Item&amp;gt;) : RecyclerView.Adapter&amp;lt;MyAdapter.ViewHolder&amp;gt;() {

    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val button: Button = view.findViewById(R.id.myButton)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.item_view, parent, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.button.setOnClickListener {
            // Listener that doesn't hold a strong reference to the context
        }
    }

    override fun onViewRecycled(holder: ViewHolder) {
        holder.button.setOnClickListener(null) // Detach listener
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;dd2d6246-471c-4374-bc27-8feebcb8bd11&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;00265a0c-baa7-4125-8c7c-24f0e0f0b709&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;83c3ce67-0108-46ec-ba7e-b9c2c050e1a5&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;709a4f68-1920-49c8-9b2c-a37a01274864&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;b6de538e-913a-47c7-934d-5cc9ab33e2dc&quot;&gt;결론은&lt;/p&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;f0eface2-9050-4e5a-a307-2923e106e67a&quot; start=&quot;1&quot; type=&quot;1&quot;&gt;&lt;li&gt;항상 뷰에 대한 약한 참조를 사용하자&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;4d6474b5-372c-4d05-8827-5eed24d5c863&quot; start=&quot;2&quot; type=&quot;1&quot;&gt;&lt;li&gt;웬만하면 context는 넘기지 말자&lt;/li&gt;&lt;/ol&gt;&lt;html&gt;&lt;body&gt;&lt;br/&gt;&lt;p class=&quot;&quot;&gt;Uploaded by &lt;mark class=&quot;highlight-orange&quot;&gt;&lt;a href=&quot;https://github.com/jmjeon94/N2T&quot;&gt;N2T&lt;/a&gt;&lt;/mark&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;&lt;/div&gt;&lt;/article&gt;&lt;span class=&quot;sans&quot; style=&quot;font-size:14px;padding-top:2em&quot;&gt;&lt;/span&gt;&lt;/body&gt;</description>
      <category>개발/Android</category>
      <category>android</category>
      <category>Kotlin</category>
      <category>MemoryLeak</category>
      <category>안드로이드</category>
      <category>코틀린</category>
      <author>이도일</author>
      <guid isPermaLink="true">https://dev-soyeon.tistory.com/102</guid>
      <comments>https://dev-soyeon.tistory.com/102#entry102comment</comments>
      <pubDate>Wed, 3 Jan 2024 10:55:31 +0900</pubDate>
    </item>
    <item>
      <title>Android SSL Pinning</title>
      <link>https://dev-soyeon.tistory.com/101</link>
      <description>&lt;body&gt;&lt;html&gt;&lt;head&gt;&lt;link href=&quot;//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.2/styles/atom-one-dark.min.css&quot; rel=&quot;stylesheet&quot;/&gt;
&lt;script src=&quot;//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.2/highlight.min.js&quot;&gt;&lt;/script&gt;
&lt;script&gt;hljs.initHighlightingOnLoad();&lt;/script&gt;&lt;/head&gt;&lt;/html&gt;&lt;link href=&quot;https://rawcdn.githack.com/ppuep94/n2t/5ef4dc01e9d6336341e9ab95bb71672f9d3a3dc9/assets/css/style2.css&quot; rel=&quot;stylesheet&quot;/&gt;&lt;article class=&quot;page sans Notion&quot; id=&quot;a2fde8b9-e999-499b-b936-b5e097eecf94&quot;&gt;&lt;header&gt;&lt;p class=&quot;page-description&quot;&gt;&lt;/p&gt;&lt;/header&gt;&lt;div class=&quot;page-body Tistory&quot;&gt;&lt;p class=&quot;&quot; id=&quot;065cc574-c101-433c-9b33-c35c2ccd4c75&quot;&gt;
&lt;/p&gt;&lt;h2 class=&quot;&quot; id=&quot;0345bed7-a86b-4b21-b4fa-2078a2a0612a&quot;&gt;Pinning&lt;/h2&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;50466be2-7452-45e8-a48b-2d8ed4ef732c&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;ssl 인증서에 의존하는 서비스나 사이트의 보안을 향상시키기 위한 선택적 메커니즘&lt;/li&gt;&lt;/ul&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;13fa44cd-b3f1-4d8f-95cf-9acb888b04ef&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;피닝을 쓰면 암호화 id와 호스트를 연결할 수 이씀.&lt;/li&gt;&lt;/ul&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;b613ebc4-7670-4a97-bc9d-2080c44a904b&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;pinning은 보안 연결을 설정할때 허용할 특정 id를 클라이언트에 알려줌 &lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;0088cd8a-e38d-47e7-940a-9f499a564cf5&quot;&gt;
&lt;/p&gt;&lt;h2 class=&quot;&quot; id=&quot;94111567-96da-4663-85f3-56cdd6139294&quot;&gt;SSL Pinning&lt;/h2&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;a8b47660-fc7e-4cca-99b5-70fe51f18fc9&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;호스트를 해당 인증서 또는 공개 키와 연결하는 프로세스&lt;/li&gt;&lt;/ul&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;03b307c7-c51b-4a78-bf11-66adbc913825&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;호스트의 인증서 / 공개키를 알게되면, 이를 해당 호스트에 pin&lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;5de2d233-198c-4007-933c-ff6b5404d31b&quot;&gt;→ 사전에 정의된 공개키가 아니면 모두 거부!&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;429f3427-42a6-40a6-b25c-f3383a178e1f&quot;&gt;
&lt;/p&gt;&lt;h2 class=&quot;&quot; id=&quot;990c6fb5-bfb0-4951-9216-54f2970cc338&quot;&gt;이걸 왜 쓰나요?&lt;/h2&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;e39a4443-13aa-449b-99d6-adfac7d70437&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;ca로 인증받는 방식에서 끝나게되면, &lt;/li&gt;&lt;/ul&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;6465d89f-cf2f-4204-a6d7-2b4d9e006990&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;디바이스의 루트 ca를 해킹하게 되거나, android trust store에 등록하게 되면, 중간에서 중간자 공격이 가능해짐&lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;62cb0106-8a62-43f9-9840-9342d5889d54&quot;&gt;→ 이를 막기위함!&lt;/p&gt;&lt;div class=&quot;indented&quot;&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;098d7d61-98bf-4e8f-ac0d-2bfa947f2d61&quot; start=&quot;1&quot; type=&quot;1&quot;&gt;&lt;li&gt;중간자 공격으로부터 보호할 수 있고&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;71f0e075-a57e-44d5-a70e-8cbce571c3ff&quot; start=&quot;2&quot; type=&quot;1&quot;&gt;&lt;li&gt;ca손상에 영향을 받지 않으며 &lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;4482988b-35de-42f8-9d13-90bed7dcdb3f&quot; start=&quot;3&quot; type=&quot;1&quot;&gt;&lt;li&gt;민감 정보를 보호할 수 있다&lt;/li&gt;&lt;/ol&gt;&lt;/div&gt;&lt;p class=&quot;&quot; id=&quot;dbee648f-5037-4d03-8e38-6461bf168cc8&quot;&gt;
&lt;/p&gt;&lt;h2 class=&quot;&quot; id=&quot;d3a0a9da-6bfa-4a80-9579-9ddf29c7f7a4&quot;&gt;SSL Pinning 유형&lt;/h2&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;dbb81796-1d60-4535-b094-cace6adbbda6&quot; start=&quot;1&quot; type=&quot;1&quot;&gt;&lt;li&gt;인증서 고정 : 서버의 인증서를 다운하여 앱에 묶음. 런타임시 앱은 서버의 인증서를 사용자가 삽입한 인증서와 비교&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;85bf82e2-39ee-4e28-b11f-1fd1f11d0794&quot; start=&quot;2&quot; type=&quot;1&quot;&gt;&lt;li&gt;공개 키 고정 : 인증서의 공개 키를 검색해 코드에 문자열로 포함할 수 있음. 런타임시 앱은 인증서의 공개 키를 코드에 하드코딩된 키와 비교&lt;/li&gt;&lt;/ol&gt;&lt;p class=&quot;&quot; id=&quot;5d4064f5-57ce-487b-83f4-49470107e578&quot;&gt;
&lt;/p&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;f95d8484-7951-48bd-bd7f-4a4dd29a2ea1&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;안드로이드에서 신뢰할 수 있는 루트 인증서를 보는 방법은&lt;ol class=&quot;numbered-list&quot; id=&quot;600e90e1-2cbf-41bc-892e-f2c13e19985b&quot; start=&quot;1&quot; type=&quot;1&quot;&gt;&lt;li&gt;설정 열기&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;0daea61f-e104-4c99-a094-43facc578993&quot; start=&quot;2&quot; type=&quot;1&quot;&gt;&lt;li&gt;보안 및 위치&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;8cabe5c6-196c-47b6-b6dd-b00a7a7ca5e8&quot; start=&quot;3&quot; type=&quot;1&quot;&gt;&lt;li&gt;암호화 및 자격 증명&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;5a5f1b02-c836-4f80-a066-38be9c0aa1be&quot; start=&quot;4&quot; type=&quot;1&quot;&gt;&lt;li&gt;신뢰할 수 있는 자격 증명&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;6b13b61d-9942-4b2d-9950-1ecf1b221a0e&quot;&gt;
&lt;/p&gt;&lt;h2 class=&quot;&quot; id=&quot;06fe5b40-7a8f-4edd-a0ec-9f5a1df324b8&quot;&gt;Android network 구성&lt;/h2&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;a4a5603c-de37-4901-9b18-adae0efad498&quot; start=&quot;1&quot; type=&quot;1&quot;&gt;&lt;li&gt;가장 간단한 network_security_config.xml 구성&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;bcdad8ae-cbbf-4de3-8ed4-1f7cdb4caa83&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;network-security-config&amp;gt;
    &amp;lt;domain-config&amp;gt;
        &amp;lt;domain includeSubdomains=&quot;true&quot;&amp;gt;example.com&amp;lt;/domain&amp;gt;
        &amp;lt;pin-set&amp;gt;
            &amp;lt;!-- Pin for the SSL/TLS certificate of example.com --&amp;gt;
            &amp;lt;pin digest=&quot;SHA-256&quot;&amp;gt;ReplaceWithYourPin&amp;lt;/pin&amp;gt;
            &amp;lt;!-- Backup Pin for the SSL/TLS certificate of example.com --&amp;gt;
            &amp;lt;pin digest=&quot;SHA-256&quot;&amp;gt;ReplaceWithYourPin&amp;lt;/pin&amp;gt;
        &amp;lt;/pin-set&amp;gt;
    &amp;lt;/domain-config&amp;gt;
&amp;lt;/network-security-config&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;01505b44-2458-4a02-b5e5-935b886d6a4a&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;여기서 핀 값은 X.509 SPKI의 base64로 인코딩된 다이제스트. &lt;/li&gt;&lt;/ul&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;190de916-a449-499e-ac6b-4a1a51a75422&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;신뢰할 수 있는 도메인에 대한 공개 키 또는 인증서의  sha-256해시값. (base64) &lt;ul class=&quot;bulleted-list&quot; id=&quot;31590edd-bd25-4a4d-ac96-f596e0409f49&quot;&gt;&lt;li style=&quot;list-style-type:circle&quot;&gt;서버의 ssl / tls인증서를 검사하여 얻거나 &lt;/li&gt;&lt;/ul&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;f24113aa-fdc4-4901-a2df-dc41ed04baeb&quot;&gt;&lt;li style=&quot;list-style-type:circle&quot;&gt;&lt;a href=&quot;https://www.ssllabs.com/ssltest/&quot;&gt;&lt;strong&gt;ssllabs 웹사이트&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt; &lt;/strong&gt;를 이용하는것도 가능!&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;89ae2db3-10a0-473e-88a4-643c0b7b9615&quot;&gt;
&lt;/p&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;1e35c433-88f6-476c-af4e-e40748cf5d8a&quot; start=&quot;2&quot; type=&quot;1&quot;&gt;&lt;li&gt;그 다음은 manifest에 해당 파일 추가&lt;/li&gt;&lt;/ol&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;37fadff1-b110-494a-b828-ecb959441956&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;&amp;lt;application
    android:networkSecurityConfig=&quot;@xml/network_security_config&quot;
    ...&amp;gt;
    ...
&amp;lt;/application&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;3dfe3ef7-2187-44e0-98ae-81fb8563f744&quot;&gt;
&lt;/p&gt;&lt;h2 class=&quot;&quot; id=&quot;ccf205d7-6168-4127-9a6a-fd127dcdfa24&quot;&gt;테스트!&lt;/h2&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;386c669f-5ced-49cc-b041-d6d2cd05a01d&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;try {
    // Create a URL object for the target server
    val mURL = URL(&quot;https://example.com/api/data&quot;)
    with(mURL.openConnection() as HttpsURLConnection) {
        requestMethod = &quot;GET&quot;
        // Add any necessary headers here
        println(&quot;URL: ${this.url}&quot;)
        println(&quot;Response Code: ${this.responseCode}&quot;)
        // Perform the actual connection and handle the response
        val responseCode = responseCode
        if (responseCode == HttpsURLConnection.HTTP_OK) {
            // Hooray! Connection successful - time to celebrate!
            // Now, let's process the response and show off our data handling skills!  
        } else {
            // Oops! Handle other response codes (e.g., error cases) gracefully  
            // Every superhero faces challenges - it's how we handle them that matters!
        }
    }
} catch (e: Throwable) {
    // Uh-oh! Invalid ssl pinning or some other Network errors - but fear not, we're prepared!  ️
    // Time to troubleshoot and save the day with proper error handling!
    println(e)
}&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;&lt;/ol&gt;&lt;html&gt;&lt;body&gt;&lt;br/&gt;&lt;p class=&quot;&quot;&gt;Uploaded by &lt;mark class=&quot;highlight-orange&quot;&gt;&lt;a href=&quot;https://github.com/jmjeon94/N2T&quot;&gt;N2T&lt;/a&gt;&lt;/mark&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;&lt;/div&gt;&lt;/article&gt;&lt;span class=&quot;sans&quot; style=&quot;font-size:14px;padding-top:2em&quot;&gt;&lt;/span&gt;&lt;/body&gt;</description>
      <category>개발/Android</category>
      <category>android</category>
      <category>Kotlin</category>
      <category>Network</category>
      <category>pinning</category>
      <category>SSL</category>
      <category>네트워크</category>
      <category>안드로이드</category>
      <category>코틀린</category>
      <author>이도일</author>
      <guid isPermaLink="true">https://dev-soyeon.tistory.com/101</guid>
      <comments>https://dev-soyeon.tistory.com/101#entry101comment</comments>
      <pubDate>Fri, 15 Dec 2023 13:53:26 +0900</pubDate>
    </item>
    <item>
      <title>Android의 모듈식 접근 방식</title>
      <link>https://dev-soyeon.tistory.com/100</link>
      <description>&lt;body&gt;&lt;html&gt;&lt;head&gt;&lt;link href=&quot;//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.2/styles/atom-one-dark.min.css&quot; rel=&quot;stylesheet&quot;/&gt;
&lt;script src=&quot;//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.2/highlight.min.js&quot;&gt;&lt;/script&gt;
&lt;script&gt;hljs.initHighlightingOnLoad();&lt;/script&gt;&lt;/head&gt;&lt;/html&gt;&lt;link href=&quot;https://rawcdn.githack.com/ppuep94/n2t/5ef4dc01e9d6336341e9ab95bb71672f9d3a3dc9/assets/css/style2.css&quot; rel=&quot;stylesheet&quot;/&gt;&lt;article class=&quot;page sans Notion&quot; id=&quot;e82c7810-abcf-49fa-958e-40bf0bd3cdf3&quot;&gt;&lt;header&gt;&lt;p class=&quot;page-description&quot;&gt;&lt;/p&gt;&lt;/header&gt;&lt;div class=&quot;page-body Tistory&quot;&gt;&lt;p class=&quot;&quot; id=&quot;66acefc2-06da-4b46-af1a-6526933d063e&quot;&gt;
&lt;/p&gt;&lt;h2 class=&quot;&quot; id=&quot;8527cdaf-ee4f-467e-bfaa-d0344c249f7e&quot;&gt;모듈의 종류&lt;/h2&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;18d5eabb-9101-43d8-ab96-30c2e718c4f8&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;App Module : 메인 모듈. ui, android 구성요소, 부트스트랩 코드 포함&lt;/li&gt;&lt;/ul&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;a65bdfda-a4eb-45ab-ada3-8dd17e487d29&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;Feature Module : 각각 앱의 기능을 담당하는 모듈. 독립적으로 개발하고 테스트가 필요한 친구들을 이렇게 구분함&lt;/li&gt;&lt;/ul&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;72ac9c6a-d18b-423f-b811-bc1364375524&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;Library Module : 네트워크 계층, 데이터베이스 서비스 또는 유틸리티 클래스와 같은 재사용 가능한 구성 요소&lt;/li&gt;&lt;/ul&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;04429c8d-53c0-4e48-b1e7-9b012ea3169a&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;Dynamic Feature Module(DFM) : 필요하지 않은 기능은 다운을 받지 않는 등, 선택적인 기능을 위한 동적 모듈 (ex : 앱 초기 설치 후 필요하지 않은 기능인 온보딩 모듈 등)&lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;b94f9505-ecac-4a8c-aa6a-81218a772c1b&quot;&gt;
&lt;/p&gt;&lt;h2 class=&quot;&quot; id=&quot;f2488bf6-b6fe-41be-8894-930bcf73039f&quot;&gt;예시 &lt;/h2&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;fb038262-4e02-440b-ab32-06a62f9c70cf&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;앱 모듈:&lt;code&gt;app&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;b882b2b2-6a1b-4125-a4cb-ee00546132b0&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;기능 모듈: &lt;code&gt;feature_login, feature_dashboard&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;723eb83f-c1a8-49b0-b784-6b6ecd0d324b&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;라이브러리 모듈: &lt;code&gt;network,database&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;3724e3ba-a63d-406f-aff5-2c7597b58d4b&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;동적 기능 모듈:&lt;code&gt;feature_extras&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;05acf706-36ba-4a9b-b6d0-d54b8dab9fcc&quot;&gt;
&lt;/p&gt;&lt;h2 class=&quot;&quot; id=&quot;273b4d62-1572-4956-a384-83e46d9d5359&quot;&gt;Feature 모듈 구현(로그인)&lt;/h2&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;5223abc0-3b18-4e69-9827-11e13eabbbdd&quot; start=&quot;1&quot; type=&quot;1&quot;&gt;&lt;li&gt;모듈 생성 :&lt;code&gt;feature_login&lt;/code&gt;&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;b2fabb4d-4dcc-4529-98ce-cfe901b8eb93&quot; start=&quot;2&quot; type=&quot;1&quot;&gt;&lt;li&gt;dependencies 설정: 필요한 경우 &lt;code&gt;network, database&lt;/code&gt; 에 설정&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;93d4af95-f0b8-4613-b4bf-017f7c05b493&quot; start=&quot;3&quot; type=&quot;1&quot;&gt;&lt;li&gt;기능 구현: 로그인과 관련된 Kotlin 클래스, 리소스, Android 구성요소 구현&lt;/li&gt;&lt;/ol&gt;&lt;p class=&quot;&quot; id=&quot;f24ab357-265e-4da8-9c5c-b2031479dfed&quot;&gt;
&lt;/p&gt;&lt;h2 class=&quot;&quot; id=&quot;ce675661-c0fc-4dd9-9d44-8ba300604240&quot;&gt;모듈간의 Communication&lt;/h2&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;5dacf171-65d2-493d-adc5-ff1cf1b28201&quot; start=&quot;1&quot; type=&quot;1&quot;&gt;&lt;li&gt;interface / DI를 사용해 모듈간의 통신을 함&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;81fcc928-384d-4d99-9b22-47eaf1fdc160&quot; start=&quot;2&quot; type=&quot;1&quot;&gt;&lt;li&gt;기능 모듈 간의 직접적인 종속성을 피하기!&lt;/li&gt;&lt;/ol&gt;&lt;p class=&quot;&quot; id=&quot;a50c2e3b-6d14-4ac1-84a6-cdd282f339fd&quot;&gt;
&lt;/p&gt;&lt;h2 class=&quot;&quot; id=&quot;62f3b094-cce3-44b2-b526-ca5ee2f6c2be&quot;&gt;테스트&lt;/h2&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;71fc1514-50fd-462b-8172-47d51f93e3db&quot; start=&quot;1&quot; type=&quot;1&quot;&gt;&lt;li&gt;각 모듈의 단위 / 통합 테스트 작성&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;91b83b9c-61be-4f4c-9fd0-3da0fd4baca3&quot; start=&quot;2&quot; type=&quot;1&quot;&gt;&lt;li&gt;모듈간의 상호 작용을 테스트&lt;/li&gt;&lt;/ol&gt;&lt;p class=&quot;&quot; id=&quot;dcbef975-e012-4dc4-a103-fe12d20612d2&quot;&gt;
&lt;/p&gt;&lt;h2 class=&quot;&quot; id=&quot;477a5b82-8505-4e26-b8fd-39edb7477004&quot;&gt;빌드 및 실행&lt;/h2&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;55b619a7-6c7f-492f-a60f-b7dd05ce6ae0&quot; start=&quot;1&quot; type=&quot;1&quot;&gt;&lt;li&gt;각 모듈은 독립적으로 컴파일 / 테스트 될 수 있음&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;c0bc60b1-03da-46c4-8663-c23f9131abce&quot; start=&quot;2&quot; type=&quot;1&quot;&gt;&lt;li&gt;앱 모듈은 모든 모듈을 단일 APK / AAR로 결합 &lt;/li&gt;&lt;/ol&gt;&lt;p class=&quot;&quot; id=&quot;d75e39cf-18ae-4915-87f0-0ec89802da6a&quot;&gt;
&lt;/p&gt;&lt;h2 class=&quot;&quot; id=&quot;80241cc8-7e93-4fb6-9014-b5c054691844&quot;&gt;장점&lt;/h2&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;2301c0a7-988d-4dbb-b9e4-66218e484759&quot; start=&quot;1&quot; type=&quot;1&quot;&gt;&lt;li&gt;확장성 : 새로운 기능 추가 쉬움&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;71d55b97-a9f0-498e-baf4-d2f2a1fb382b&quot; start=&quot;2&quot; type=&quot;1&quot;&gt;&lt;li&gt;유지보수 : 한 모듈 변경해도 다른 모듈은 영향 받지않음&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;b68e0aa7-661e-495e-bbe2-713fa6687e9d&quot; start=&quot;3&quot; type=&quot;1&quot;&gt;&lt;li&gt;더 빠른 빌드시간 : 변경된 모듈만 리컴파일 &lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;c6e31cff-17eb-4b35-aeb2-d192bdc30259&quot; start=&quot;4&quot; type=&quot;1&quot;&gt;&lt;li&gt;협업 : 여러명의 개발자가 서로 다른 모듈 동시 작업 가능&lt;/li&gt;&lt;/ol&gt;&lt;p class=&quot;&quot; id=&quot;3794fee1-fb6c-4a9e-a443-f72937eb4767&quot;&gt;
&lt;/p&gt;&lt;h2 class=&quot;&quot; id=&quot;475a86c6-a24a-4491-b5fa-536367b01cf9&quot;&gt;주의사항&lt;/h2&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;bd51d371-39f0-4612-b16c-898910c57b4e&quot; start=&quot;1&quot; type=&quot;1&quot;&gt;&lt;li&gt;모듈에 집중하고, 종속성 피하기&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;46fbaaea-bff9-481f-b155-99f2af5ddb3c&quot; start=&quot;2&quot; type=&quot;1&quot;&gt;&lt;li&gt;gradle / dependency는 주기적으로 업데이트하기&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;146bb4dd-af3f-4ce8-a120-4d948300b160&quot; start=&quot;3&quot; type=&quot;1&quot;&gt;&lt;li&gt;모듈 전반에 일관된 코딩 컨벤션을 적용하기&lt;/li&gt;&lt;/ol&gt;&lt;p class=&quot;&quot; id=&quot;471f06fd-a373-4248-868b-3098540d41ae&quot;&gt;
&lt;/p&gt;&lt;h2 class=&quot;&quot; id=&quot;f647bca8-dff9-44f5-8ba6-4bb9a6db313d&quot;&gt;Example&lt;/h2&gt;&lt;blockquote class=&quot;&quot; id=&quot;bae7146e-a405-4ebd-9ac1-ca877b9014d0&quot;&gt;&lt;strong&gt;Root &lt;/strong&gt;&lt;/blockquote&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;d9994ab2-1706-4e84-934e-ea4b2a8c9602&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;&lt;code&gt;settings.gradle&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;e7c4a95a-f735-4513-a47e-f13b7ca95876&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;&lt;code&gt;build.gradle&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;b19b6989-1667-4333-a3a6-7c65ea5d8335&quot;&gt;
&lt;/p&gt;&lt;blockquote class=&quot;&quot; id=&quot;6b721133-972d-482b-a244-87a3098dfcaa&quot;&gt;&lt;strong&gt;App Module (app)&lt;/strong&gt;&lt;/blockquote&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;9cdd4e71-9439-4a17-bb85-019387877eaf&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;앱의 메인 진입점&lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;a61f3aba-77eb-4999-a79a-7e987dc39b6c&quot;&gt;
&lt;/p&gt;&lt;blockquote class=&quot;&quot; id=&quot;d52792aa-d4e7-4a93-835f-ed803b01c7c3&quot;&gt;&lt;strong&gt;Feature Modules&lt;/strong&gt;&lt;/blockquote&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;a5730aaf-0000-4529-8ee4-b5198cf18c70&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;&lt;code&gt;feature_login&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;9ad9fc85-5441-41c1-b0b4-1c5d3dff59a0&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;&lt;code&gt;feature_dashboard&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;6eac1738-4c27-4bbe-9c7e-f3f006e209c9&quot;&gt;
&lt;/p&gt;&lt;blockquote class=&quot;&quot; id=&quot;c5f3f7b6-ee09-4e3c-a2fa-8c051dab7d8e&quot;&gt;&lt;strong&gt;Library Module (core)&lt;/strong&gt;&lt;/blockquote&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;0093c020-35c6-4425-a995-cc09d7b17d06&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;네트워크 서비스, 유틸 등의 모듈 위치&lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;c1cd8313-6e38-4c11-9c03-9eb49c1b8793&quot;&gt;
&lt;/p&gt;&lt;blockquote class=&quot;&quot; id=&quot;1f7e822e-93f5-4389-a1e7-96a942ff2723&quot;&gt;&lt;strong&gt;settings.gradle&lt;/strong&gt;&lt;/blockquote&gt;&lt;p class=&quot;&quot; id=&quot;08aeaa91-3c71-417a-8658-b0c870478a07&quot;&gt;모든 모듈을 포함하기&lt;/p&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code code-wrap Groovy&quot; id=&quot;2beb5ce0-856b-4a60-98e1-7444afa6afe2&quot;&gt;&lt;code class=&quot;language-Groovy&quot;&gt;include ':app', ':feature_login', ':feature_dashboard', ':core'&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;7e55c9e9-e55c-450e-9088-b30c01e55096&quot;&gt;
&lt;/p&gt;&lt;blockquote class=&quot;&quot; id=&quot;346dec13-e990-47b7-b6b8-09d86f4c3a47&quot;&gt;&lt;strong&gt;App Module (app)&lt;/strong&gt;&lt;/blockquote&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;2a81cdc3-094d-4b0b-811e-3dc43c6dd601&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;build.gradle (app)&lt;/li&gt;&lt;/ul&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;976cd674-1676-4461-afad-8c94bdda5cef&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;feature modules + core library.&lt;/li&gt;&lt;/ul&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code code-wrap Groovy&quot; id=&quot;bd0dc35f-68e5-4067-9655-a8d1f6253d2f&quot;&gt;&lt;code class=&quot;language-Groovy&quot;&gt;dependencies {
     implementation project(':feature_login')
     implementation project(':feature_dashboard')
     implementation project(':core')
}&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;591ff20d-9b3a-4f30-9ad6-3402665e3e2d&quot;&gt;
&lt;/p&gt;&lt;blockquote class=&quot;&quot; id=&quot;a879b442-51e9-4b84-8892-f7035ead8a58&quot;&gt;&lt;strong&gt;Feature Module (feature_login)&lt;/strong&gt;&lt;/blockquote&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;e7e98405-4f8f-4a74-a569-abdde520ce35&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;build.gradle (feature_login)&lt;/li&gt;&lt;/ul&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;57eb2c3c-a827-498b-b328-131a0d167ab3&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;login feature에 한정된 dependencies&lt;/li&gt;&lt;/ul&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code code-wrap Groovy&quot; id=&quot;4f26cdc2-85bd-49f4-9d0f-bc0490c8f6f8&quot;&gt;&lt;code class=&quot;language-Groovy&quot;&gt;dependencies {
     implementation project(':core')
     // Core module for shared functionality
     // ... other dependencies
}&lt;/code&gt;&lt;/pre&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;82c07457-e353-405f-b420-5a9179bbe1fc&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;LoginActivity.kt&lt;/li&gt;&lt;/ul&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code code-wrap Kotlin&quot; id=&quot;ce32335c-0f99-4144-9c51-94526d50fc44&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;package com.example.feature_login
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
class LoginActivity : AppCompatActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_login)
         // Login logic here
     }
}&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;106b7068-0ee0-426a-9856-68bc1fe6e91e&quot;&gt;
&lt;/p&gt;&lt;blockquote class=&quot;&quot; id=&quot;65f47d34-8e3a-48c3-941f-8c017cc37a83&quot;&gt;&lt;strong&gt;Feature Module (feature_dashboard)&lt;/strong&gt;&lt;/blockquote&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;da73a8f8-4d77-4c45-b7a9-1b58f80cb424&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;&lt;code&gt;feature_login&lt;/code&gt; 랑 비슷하나, dashboard features에 특정되는것~&lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;48ed5489-020a-4dcb-a563-9e7b2d07dc41&quot;&gt;
&lt;/p&gt;&lt;blockquote class=&quot;&quot; id=&quot;9fe4bb5e-0914-4e51-9e51-1aaa7139a99b&quot;&gt;&lt;strong&gt;Library Module (core)&lt;/strong&gt;&lt;/blockquote&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;1814b856-5167-4016-a58c-52e073aa5f7a&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;build.gradle (core)&lt;/li&gt;&lt;/ul&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;4e2eaa6f-5fb2-4d1d-815f-9db8d40151bb&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;Retrofit, Room, etc.&lt;/li&gt;&lt;/ul&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code code-wrap Groovy&quot; id=&quot;a9dcc4a6-3a06-42d6-84a0-f7cfbfd82028&quot;&gt;&lt;code class=&quot;language-Groovy&quot;&gt;dependencies {     // Shared libraries like Retrofit, Room }&lt;/code&gt;&lt;/pre&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;213bf884-9595-45b3-8185-ab12afff82a6&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;NetworkService.kt&lt;/li&gt;&lt;/ul&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code code-wrap Kotlin&quot; id=&quot;ba3fde6c-b65d-4460-a6a2-f9a3f870e93d&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;package com.example.core.network  class NetworkService {
     // Implementation of network calls
}&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;e1feedb3-5065-48e2-8606-34f9611eb304&quot;&gt;
&lt;/p&gt;&lt;html&gt;&lt;body&gt;&lt;br/&gt;&lt;p class=&quot;&quot;&gt;Uploaded by &lt;mark class=&quot;highlight-orange&quot;&gt;&lt;a href=&quot;https://github.com/jmjeon94/N2T&quot;&gt;N2T&lt;/a&gt;&lt;/mark&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;&lt;/div&gt;&lt;/article&gt;&lt;span class=&quot;sans&quot; style=&quot;font-size:14px;padding-top:2em&quot;&gt;&lt;/span&gt;&lt;/body&gt;</description>
      <category>개발/Android</category>
      <category>android</category>
      <category>CleanArchitecture</category>
      <category>DFM</category>
      <category>DI</category>
      <category>Kotlin</category>
      <category>Module</category>
      <category>안드로이드</category>
      <category>코틀린</category>
      <author>이도일</author>
      <guid isPermaLink="true">https://dev-soyeon.tistory.com/100</guid>
      <comments>https://dev-soyeon.tistory.com/100#entry100comment</comments>
      <pubDate>Fri, 15 Dec 2023 13:53:17 +0900</pubDate>
    </item>
    <item>
      <title>MenuProvider</title>
      <link>https://dev-soyeon.tistory.com/99</link>
      <description>&lt;body&gt;&lt;html&gt;&lt;head&gt;&lt;link href=&quot;//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.2/styles/atom-one-dark.min.css&quot; rel=&quot;stylesheet&quot;/&gt;
&lt;script src=&quot;//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.2/highlight.min.js&quot;&gt;&lt;/script&gt;
&lt;script&gt;hljs.initHighlightingOnLoad();&lt;/script&gt;&lt;/head&gt;&lt;/html&gt;&lt;link href=&quot;https://rawcdn.githack.com/ppuep94/n2t/5ef4dc01e9d6336341e9ab95bb71672f9d3a3dc9/assets/css/style2.css&quot; rel=&quot;stylesheet&quot;/&gt;&lt;article class=&quot;page sans Notion&quot; id=&quot;3cbb553d-5bc9-48ce-9251-ad771884d1a4&quot;&gt;&lt;header&gt;&lt;p class=&quot;page-description&quot;&gt;&lt;/p&gt;&lt;/header&gt;&lt;div class=&quot;page-body Tistory&quot;&gt;&lt;p class=&quot;&quot; id=&quot;7eba1760-c657-42a9-b5eb-d5c250b96717&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;7ccc8664-c6e8-478c-a50b-a4368ed5dd15&quot;&gt;&lt;code&gt;&lt;strong&gt;&lt;strong&gt;setHasOptionsMenu()&lt;/strong&gt;&lt;/strong&gt;&lt;/code&gt;&lt;strong&gt;&lt;strong&gt; , onCreateOptionsMenu(), onOptionsItemSelected() 는 Deprecated 되었다…………….&lt;/strong&gt;&lt;/strong&gt;&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;87243b9a-1d79-4576-a1c7-e8544e5750cd&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;d22ca685-51af-4295-a4fa-2e1eba1cbb9b&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;94858f03-d352-4e92-bb63-91d77ae20166&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;a44c23e7-2fed-4c0a-b9a2-42cd7a66e13b&quot;&gt;그렇다면 대체제는 무엇일까?&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;8cc2f1bf-b562-4ae2-a286-4ce441d694cb&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;16c26b8c-3128-4d03-a944-6c669f926bdc&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;d52df7c2-b85f-40d2-bb20-0b8298fc5f54&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;77b6000d-0624-4424-a28d-167cc013bc5b&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;5c621651-e053-4d9b-9026-445ef2cefd43&quot;&gt;&lt;strong&gt;&lt;strong&gt; &lt;/strong&gt;&lt;/strong&gt;&lt;/p&gt;&lt;h2 class=&quot;&quot; id=&quot;92a12756-6e68-46d0-a650-474a92f4e9c0&quot;&gt;MenuProvider&lt;/h2&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;0d077d72-17ec-43c4-9ff5-03782cc8e282&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;4개의 메소드를 포함한 인터페이스이다&lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;57d204ab-5b5a-4832-bf78-ec2e57f7e3dd&quot;&gt;
&lt;/p&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;a2dc033c-d5de-4189-931d-ba0cff74a12b&quot; start=&quot;1&quot; type=&quot;1&quot;&gt;&lt;li&gt;onCreateMenu : menu를 inflate하는 부분&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;1325b34b-c683-48a0-9f68-1be14453aed9&quot; start=&quot;2&quot; type=&quot;1&quot;&gt;&lt;li&gt;onMenuItemSelected : option메뉴를 클릭할 때 발생하는 부분&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;93528971-4a4a-472d-9def-5fa747887763&quot; start=&quot;3&quot; type=&quot;1&quot;&gt;&lt;li&gt;onPrepareMenu : menu item을 customizing하는 부분. visibility나 menuItem의 customize를 할 수 있다&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;9950e71d-ab35-4442-b387-c00d178f3242&quot; start=&quot;4&quot; type=&quot;1&quot;&gt;&lt;li&gt;onMenuClosed : 메뉴 닫히면 발생하는 부분&lt;/li&gt;&lt;/ol&gt;&lt;p class=&quot;&quot; id=&quot;032adf26-0252-42a8-a7ca-4ae69d7fd79e&quot;&gt;
&lt;/p&gt;&lt;h2 class=&quot;&quot; id=&quot;7ef5341b-f680-4434-a2c9-690e5573e2c2&quot;&gt;MenuHost&lt;/h2&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;44521081-3057-44bc-851b-2c59b639e684&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;ComponentActivity, AppCompatActivity에 implement된 인터페이스. menu provider를 사용해 추가된 메뉴들의 control 메소드를 제공함&lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;9cacfa00-686c-4ca2-b809-fac1726ce901&quot;&gt;
&lt;/p&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;f06df75e-a695-4987-88ef-1ce76a0329fa&quot; start=&quot;1&quot; type=&quot;1&quot;&gt;&lt;li&gt;addMenuProvider() : 3개의 케이스로 사용 가능하다 &lt;/li&gt;&lt;/ol&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;9f27d0ac-87a4-4594-9f68-77cce0c92afc&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;// 1. It only takes 1 parameter - [MenuProvider]

/*
* We need to manually remove the MenuProvider by calling removeMenuProvider().
* */
addMenuProvider(object : MenuProvider {
    override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
        menuInflater.inflate(R.menu.main_menu, menu)
    }
    override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
        return when (menuItem.itemId) {
            R.id.settings -&amp;gt; {
                true
            }
            else -&amp;gt; false
        }
    }
})

// 2. It takes 2 parameters - [MenuProvider, LifeCycleOwner]

/*
* Adds the given MenuProvider to the MenuHost. 
  MenuProvider will be removed once the given LifecycleOwner receives an Lifecycle.Event.ON_DESTROY event.
* */
addMenuProvider(object : MenuProvider {
    override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
        menuInflater.inflate(R.menu.main_menu, menu)
    }
    override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
        return when (menuItem.itemId) {
            R.id.settings -&amp;gt; {
                Toast.makeText(this@MainActivity, &quot;Settings...&quot;, Toast.LENGTH_SHORT).show()
                true
            }
            else -&amp;gt; false
        }
    }
}, /*lifecycleOwner*/ this)


// 3. It takes 3 parameters - [MenuProvider, LifeCycleOwner, Lifecycle.State]

/*
* Adds a MenuProvider to the MenuHost when a given LifecycleOwner reaches a given Lifecycle.State. 
  LifecycleOwners will be removed once they reach a lower Lifecycle.State.
* */
addMenuProvider(object : MenuProvider {
    override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
        menuInflater.inflate(R.menu.main_menu, menu)
    }
    override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
        return when (menuItem.itemId) {
            R.id.settings -&amp;gt; {
                Toast.makeText(this@MainActivity, &quot;Settings...&quot;, Toast.LENGTH_SHORT).show()
                true
            }
            else -&amp;gt; false
        }
    }
}, /*lifecycleOwner*/ this, /*Lifecycle.State*/ Lifecycle.State.RESUMED)&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;927509c0-619f-46e0-86a8-5bea30385195&quot;&gt;
&lt;/p&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;fadc26b3-042c-4737-8f9b-ab9521756c31&quot; start=&quot;2&quot; type=&quot;1&quot;&gt;&lt;li&gt;invalidateMenu()&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;858eb9e3-d3ce-41ac-a092-4e558310bb27&quot; start=&quot;3&quot; type=&quot;1&quot;&gt;&lt;li&gt;removeMenuProvider()&lt;/li&gt;&lt;/ol&gt;&lt;p class=&quot;&quot; id=&quot;94796852-f83c-408b-bf3b-aa10b89d52c7&quot;&gt;
&lt;/p&gt;&lt;h2 class=&quot;&quot; id=&quot;d13981ee-96cc-4dba-a243-f481fd9298a2&quot;&gt;Activity에 menu를 등록하기 &lt;/h2&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;b9074379-114f-44bf-97b6-b704ab3a8bf2&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;addMenuProvider(object : MenuProvider {
    override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
        menuInflater.inflate(R.menu.main_menu, menu)
    }
    override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
        return when (menuItem.itemId) {
            R.id.settings -&amp;gt; {
                Toast.makeText(this@MainActivity, &quot;Settings...&quot;, Toast.LENGTH_SHORT).show()
                true
            }
            else -&amp;gt; false
        }
    }
})&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;e5fa030f-11c8-4642-9d4b-e779d46232ed&quot;&gt;
&lt;/p&gt;&lt;h2 class=&quot;&quot; id=&quot;f88efb74-a097-4246-827e-c544240158ac&quot;&gt;Fragment에 menu를 등록하기&lt;/h2&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;17eed964-67b8-4e41-bde5-3c8ab5c558c4&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    // Add options menu to the toolbar
    requireActivity().addMenuProvider(object : MenuProvider {
        override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
            menuInflater.inflate(R.menu.main_menu, menu)
        }
        override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
            return when (menuItem.itemId) {
                R.id.settings -&amp;gt; {
                    true
                }
                else -&amp;gt; false
            }
        }
    }, viewLifecycleOwner)
}&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;f56e8b3e-7d3a-4ba2-9e0e-4c5b5cb93e60&quot;&gt;
&lt;/p&gt;&lt;html&gt;&lt;body&gt;&lt;br/&gt;&lt;p class=&quot;&quot;&gt;Uploaded by &lt;mark class=&quot;highlight-orange&quot;&gt;&lt;a href=&quot;https://github.com/jmjeon94/N2T&quot;&gt;N2T&lt;/a&gt;&lt;/mark&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;&lt;/div&gt;&lt;/article&gt;&lt;span class=&quot;sans&quot; style=&quot;font-size:14px;padding-top:2em&quot;&gt;&lt;/span&gt;&lt;/body&gt;</description>
      <category>개발/Android</category>
      <category>android</category>
      <category>Kotlin</category>
      <category>MenuProvider</category>
      <category>안드로이드</category>
      <category>코틀린</category>
      <author>이도일</author>
      <guid isPermaLink="true">https://dev-soyeon.tistory.com/99</guid>
      <comments>https://dev-soyeon.tistory.com/99#entry99comment</comments>
      <pubDate>Fri, 15 Dec 2023 13:53:09 +0900</pubDate>
    </item>
    <item>
      <title>메모리 누수의 원인 10가지</title>
      <link>https://dev-soyeon.tistory.com/98</link>
      <description>&lt;body&gt;&lt;html&gt;&lt;head&gt;&lt;link href=&quot;//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.2/styles/atom-one-dark.min.css&quot; rel=&quot;stylesheet&quot;/&gt;
&lt;script src=&quot;//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.2/highlight.min.js&quot;&gt;&lt;/script&gt;
&lt;script&gt;hljs.initHighlightingOnLoad();&lt;/script&gt;&lt;/head&gt;&lt;/html&gt;&lt;link href=&quot;https://rawcdn.githack.com/ppuep94/n2t/5ef4dc01e9d6336341e9ab95bb71672f9d3a3dc9/assets/css/style2.css&quot; rel=&quot;stylesheet&quot;/&gt;&lt;article class=&quot;page sans Notion&quot; id=&quot;235d0939-80a1-406b-8fd7-3774794fe193&quot;&gt;&lt;header&gt;&lt;p class=&quot;page-description&quot;&gt;&lt;/p&gt;&lt;/header&gt;&lt;div class=&quot;page-body Tistory&quot;&gt;&lt;p class=&quot;&quot; id=&quot;8e1c9ff3-1c90-4a12-adff-a2a643559cbb&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;c3e28cb5-1938-4108-b459-be5b02116f54&quot;&gt;
&lt;/p&gt;&lt;h1 class=&quot;&quot; id=&quot;0c1751bb-c8d7-4f32-bb25-2b887986803f&quot;&gt;&lt;strong&gt;1. Non-Static Inner Classes&lt;/strong&gt;&lt;/h1&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;3997b8b9-d8b3-4581-a6a0-0855c244a86a&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;Kotlin의 내부 클래스는 외부 클래스에 대한 암시적 참조를 보유할 수 있음&lt;/li&gt;&lt;/ul&gt;&lt;blockquote class=&quot;&quot; id=&quot;fb085d2c-37b0-493a-a863-afac568aa071&quot;&gt;Example:&lt;/blockquote&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code code-wrap Kotlin&quot; id=&quot;da311874-8c3f-44db-bab0-7dc232e18a67&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;class MyActivity : AppCompatActivity() {
     private inner class MyThread : Thread() {
         override fun run() {
             // Task
         }
     }
}&lt;/code&gt;&lt;/pre&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;daeed73b-4074-47db-9baa-28e4025de623&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;해결 방법: 내부 클래스를 정적으로 만들거나, 별도의 클래스를 사용하기. 필요한 경우 외부 클래스에 대한 약한 참조를 전달&lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;2eb3fc4f-aa9b-4af0-940a-a51f8d951a85&quot;&gt;
&lt;/p&gt;&lt;h1 class=&quot;&quot; id=&quot;e7a32133-de62-4016-a6b5-499a3e6179df&quot;&gt;&lt;strong&gt;2. Handlers and Runnables&lt;/strong&gt;&lt;/h1&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;5b3e1802-5b29-4dc7-9426-0b3883163bd2&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;핸들러가 외부 클래스에 대한 참조를 보유하는 경우 메모리 누수가 발생할 수 있음&lt;/li&gt;&lt;/ul&gt;&lt;blockquote class=&quot;&quot; id=&quot;f8570e0a-f15c-4ef4-90b4-39e760d81ef0&quot;&gt;Example:&lt;/blockquote&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code code-wrap Kotlin&quot; id=&quot;17ae213d-3ff4-4e6f-b08d-8617320ca547&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;class MyActivity : AppCompatActivity() {
     private val handler = Handler(Looper.getMainLooper())
     private val runnable = Runnable { /* Task */ }
     override fun onDestroy() {
         super.onDestroy()
         handler.removeCallbacks(runnable)
     }
}&lt;/code&gt;&lt;/pre&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;f904fe9e-16c8-46b0-b78c-f04e968a15a1&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;해결 방법: &lt;code&gt;onDestroy&lt;/code&gt;메서드에서 콜백을 모두 제거&lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;2348c6b9-4916-4e7f-af5f-2694dea13eee&quot;&gt;
&lt;/p&gt;&lt;h1 class=&quot;&quot; id=&quot;a9c93c93-39d9-43a3-83a3-d5fb3d6c3588&quot;&gt;&lt;strong&gt;3. Anonymous Listeners&lt;/strong&gt;&lt;/h1&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;0e896151-1fd4-46d2-81f1-db0025cb12f4&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;리스너가 실수로 Activity나 View에 대한 참조를 보유할 수 있음&lt;/li&gt;&lt;/ul&gt;&lt;blockquote class=&quot;&quot; id=&quot;dfe04e2f-28b2-4f23-a03a-b6d1d9156f67&quot;&gt;Example:&lt;/blockquote&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code code-wrap Kotlin&quot; id=&quot;88578b57-4f39-4eb4-92a1-980febd9f702&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;class MyActivity : AppCompatActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         findViewById&amp;lt;Button&amp;gt;(R.id.myButton).setOnClickListener {
             // Click action
         }
     }
}&lt;/code&gt;&lt;/pre&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;f07f8bad-06a4-48cc-b8cb-2f5e68da3a81&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;해결방법: onDestroy 메소드에서 리스너를 지우거나 정적 클래스를 사용&lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;7bd8aede-e8db-42ec-888b-08b8cdb898f9&quot;&gt;
&lt;/p&gt;&lt;h1 class=&quot;&quot; id=&quot;1b1b5324-4826-4c29-b434-9f755d1a1425&quot;&gt;&lt;strong&gt;4. Static Views or Contexts&lt;/strong&gt;&lt;/h1&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;7993dfec-9336-4114-91cd-dedbfdfc9b55&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;정적 뷰 또는 컨텍스트 참조는 메모리 누수로 이어질 수 있음&lt;/li&gt;&lt;/ul&gt;&lt;blockquote class=&quot;&quot; id=&quot;29d462b9-f069-4cae-9df4-fe818633506e&quot;&gt;Example:&lt;/blockquote&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code code-wrap Kotlin&quot; id=&quot;6707d6e4-0eab-4083-8100-58ec61783f80&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;class MyActivity : AppCompatActivity() {
     companion object {
         private var staticView: View? = null
     }
     override fun onCreate(savedInstanceState: Bundle?) {
         staticView = findViewById(R.id.myView)
     }
}&lt;/code&gt;&lt;/pre&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;aa6ef252-fef1-476e-b9f2-88693cb9f643&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;해결 방법: 뷰나 컨텍스트에 대한 정적 참조를 피하고, 필요한 경우 약한 참조를 사용&lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;85ca0090-4de8-4597-866d-ba86564930b6&quot;&gt;
&lt;/p&gt;&lt;h1 class=&quot;&quot; id=&quot;d31300b9-d20a-4f74-abf3-1325059384cf&quot;&gt;&lt;strong&gt;5. Improper LiveData Observations&lt;/strong&gt;&lt;/h1&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;3d0f80af-f459-4559-9bd6-9c857fe6f3bd&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;수명주기를 고려하지 않고 LiveData를 관찰함&lt;/li&gt;&lt;/ul&gt;&lt;blockquote class=&quot;&quot; id=&quot;accaeeda-b976-4435-a33b-e9f8895954eb&quot;&gt;Example:&lt;/blockquote&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code code-wrap Kotlin&quot; id=&quot;bcfe5246-0267-4e99-85e2-3db38d249598&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;class MyActivity : AppCompatActivity() {
     private val viewModel: MyViewModel by viewModels()
      override fun onCreate(savedInstanceState: Bundle?) {
         viewModel.myLiveData.observe(this, { data -&amp;gt;
             /* Update UI */
          })
     }
}&lt;/code&gt;&lt;/pre&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;7c753b9e-e993-494a-a132-7c0216e6665c&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;해결 방법: &lt;code&gt;viewLifecycleOwner&lt;/code&gt;Fragments와 같은 수명 주기 인식 구성 요소를 사용하여 LiveData를 관찰&lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;1b904bd3-704f-41f7-b17f-c463bc1c67b8&quot;&gt;
&lt;/p&gt;&lt;h1 class=&quot;&quot; id=&quot;678be791-4b17-4c69-9171-3e7f75a77215&quot;&gt;&lt;strong&gt;6. Singleton with Context&lt;/strong&gt;&lt;/h1&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;adf2a1e3-da06-4814-ac2d-784eaa1e0f87&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;컨텍스트에 대한 참조를 보유하는 싱글톤은 누출을 일으킬 수 있음&lt;/li&gt;&lt;/ul&gt;&lt;blockquote class=&quot;&quot; id=&quot;07c23ec3-46f2-400b-8ec0-5b9f287a1b6a&quot;&gt;Example:&lt;/blockquote&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code code-wrap Kotlin&quot; id=&quot;9c8a1392-49d6-4dc3-b4e2-925838f8584f&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;object MySingleton {
     var context: Context? = null
}&lt;/code&gt;&lt;/pre&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;71322bb0-21b3-4531-882a-155d811359ec&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;해결 방법: Activity나 View컨텍스트 대신, 애플리케이션 컨텍스트를 싱글톤에 전달&lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;5c95ac88-e027-458e-b086-2953639c4a6a&quot;&gt;
&lt;/p&gt;&lt;h1 class=&quot;&quot; id=&quot;3980566b-8e73-4c8a-b15a-02c616c959ef&quot;&gt;&lt;strong&gt;7. Bitmaps&lt;/strong&gt;&lt;/h1&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;82b179ab-c94a-4d88-9658-7278c661d64e&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;대형 비트맵을 제대로 관리하지 않으면 많은 메모리를 소비할 수 있음&lt;/li&gt;&lt;/ul&gt;&lt;blockquote class=&quot;&quot; id=&quot;877e18cd-8252-41b1-9f1f-78a8a85bd5e3&quot;&gt;Example:&lt;/blockquote&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code code-wrap Kotlin&quot; id=&quot;81ba6116-e9b6-4a7c-beb9-a78c1f27c287&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;class MyActivity : AppCompatActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         val bitmap = BitmapFactory.decodeResource(resources, R.drawable.large_image)
         // Use bitmap
     }
}&lt;/code&gt;&lt;/pre&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;a5ed03ad-e6cc-41bc-bc62-e6d856a0683e&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;해결방법: 비트맵을 신중하게 사용하고 &lt;code&gt;recycle()&lt;/code&gt;완료되면 호출. Glide나 Picasso와 같은 이미지 로딩 라이브러리를 사용&lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;7b1748f6-6692-48e5-8b8d-20476b762641&quot;&gt;
&lt;/p&gt;&lt;h1 class=&quot;&quot; id=&quot;55d0a085-5a22-4dd5-adc9-7bcd6dadb5a7&quot;&gt;&lt;strong&gt;8. WebViews&lt;/strong&gt;&lt;/h1&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;ac35419c-bbc6-4752-9318-a4dfbd8323cb&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;WebView는 컨텍스트에 대한 참조를 보유할 수 있음&lt;/li&gt;&lt;/ul&gt;&lt;blockquote class=&quot;&quot; id=&quot;63225fcb-460d-4b6d-94ea-2a09d62e61d0&quot;&gt;Example:&lt;/blockquote&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code code-wrap Kotlin&quot; id=&quot;1cef0bf7-81fe-443e-a327-c5f8840d38c8&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;class MyActivity : AppCompatActivity() {
     private lateinit var webView: WebView
     override fun onCreate(savedInstanceState: Bundle?) {
         webView = findViewById(R.id.myWebView)
         // Set up WebView
     }
}&lt;/code&gt;&lt;/pre&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;acea9deb-f4cb-4b32-8d95-79ca3ea532f0&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;해결 방법: onDestroy 메소드에서 WebView를 지우고, 가능한 경우 애플리케이션 컨텍스트를 사용&lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;7e530d20-0302-41fd-ba5d-42fa20bcdd30&quot;&gt;
&lt;/p&gt;&lt;h1 class=&quot;&quot; id=&quot;87c03121-263e-4624-a422-a97fd8f6987e&quot;&gt;&lt;strong&gt;9. Broadcast Receivers&lt;/strong&gt;&lt;/h1&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;01d99158-8fe1-4ad1-a16b-58f5e894ae40&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;broadcast receivers등록을 취소하지 않으면 누출이 발생할 수 있음&lt;/li&gt;&lt;/ul&gt;&lt;blockquote class=&quot;&quot; id=&quot;6a1e6daa-d135-4b7f-aa83-c10369c60f7c&quot;&gt;Example:&lt;/blockquote&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code code-wrap Kotlin&quot; id=&quot;b8318a78-6472-4dac-8d18-160ad1a55b97&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;class MyActivity : AppCompatActivity() {
     private val receiver = MyReceiver()
      override fun onStart() {
         registerReceiver(receiver, IntentFilter(&quot;SOME_ACTION&quot;))
     }
     override fun onStop() {
         unregisterReceiver(receiver)
         super.onStop()
     }
}&lt;/code&gt;&lt;/pre&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;9e23bf1a-6069-48b9-a94f-8082d1551879&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;해결 방법: 항상 해당 수명 주기 메서드에서 등록을 취소하기&lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;c7a6ebde-b510-43ee-b268-c3a045838669&quot;&gt;
&lt;/p&gt;&lt;h1 class=&quot;&quot; id=&quot;5d1535da-498a-4d9b-af0d-784150df9c0c&quot;&gt;&lt;strong&gt;10. Event Listeners in RecyclerView Adapters&lt;/strong&gt;&lt;/h1&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;636a6ab9-7ff0-47bf-a3f5-b6276333a2dc&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;RecyclerView 어댑터의 이벤트 리스너는 Activity 또는 Fragment에 대한 참조를 보유할 수 있음&lt;/li&gt;&lt;/ul&gt;&lt;blockquote class=&quot;&quot; id=&quot;c1c294cb-85bd-40af-9e50-7b216239afb3&quot;&gt;Example:&lt;/blockquote&gt;&lt;script crossorigin=&quot;anonymous&quot; integrity=&quot;sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==&quot; referrerpolicy=&quot;no-referrer&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js&quot;&gt;&lt;/script&gt;&lt;link crossorigin=&quot;anonymous&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css&quot; integrity=&quot;sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==&quot; referrerpolicy=&quot;no-referrer&quot; rel=&quot;stylesheet&quot;/&gt;&lt;pre class=&quot;code code-wrap Kotlin&quot; id=&quot;c624c59c-07f2-4b9e-884b-c41b8bd944b0&quot;&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;class MyAdapter(private val items: List&amp;lt;Item&amp;gt;, private val activity: AppCompatActivity) : RecyclerView.Adapter&amp;lt;MyViewHolder&amp;gt;() {
     // Adapter implementation
}&lt;/code&gt;&lt;/pre&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;b61ee4f2-606e-4193-b4db-765b7cfe3e5c&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;해결 방법: 콜백에 인터페이스나 람다 함수를 사용하고, Activity나 Fragment 컨텍스트를 어댑터에 전달하지 않기.&lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;6495bd93-7b89-42ff-8e51-3bf0b9c4d733&quot;&gt;
&lt;/p&gt;&lt;html&gt;&lt;body&gt;&lt;br/&gt;&lt;p class=&quot;&quot;&gt;Uploaded by &lt;mark class=&quot;highlight-orange&quot;&gt;&lt;a href=&quot;https://github.com/jmjeon94/N2T&quot;&gt;N2T&lt;/a&gt;&lt;/mark&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;&lt;/div&gt;&lt;/article&gt;&lt;span class=&quot;sans&quot; style=&quot;font-size:14px;padding-top:2em&quot;&gt;&lt;/span&gt;&lt;/body&gt;</description>
      <category>개발/Android</category>
      <author>이도일</author>
      <guid isPermaLink="true">https://dev-soyeon.tistory.com/98</guid>
      <comments>https://dev-soyeon.tistory.com/98#entry98comment</comments>
      <pubDate>Fri, 15 Dec 2023 13:53:02 +0900</pubDate>
    </item>
    <item>
      <title>Coroutine Flow BackPressure &amp;amp; Buffer</title>
      <link>https://dev-soyeon.tistory.com/97</link>
      <description>&lt;body&gt;&lt;html&gt;&lt;head&gt;&lt;link href=&quot;//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.2/styles/atom-one-dark.min.css&quot; rel=&quot;stylesheet&quot;/&gt;
&lt;script src=&quot;//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.2/highlight.min.js&quot;&gt;&lt;/script&gt;
&lt;script&gt;hljs.initHighlightingOnLoad();&lt;/script&gt;&lt;/head&gt;&lt;/html&gt;&lt;link href=&quot;https://rawcdn.githack.com/ppuep94/n2t/5ef4dc01e9d6336341e9ab95bb71672f9d3a3dc9/assets/css/style2.css&quot; rel=&quot;stylesheet&quot;/&gt;&lt;article class=&quot;page sans Notion&quot; id=&quot;d6fa3c2c-9243-4701-aad9-6a7d3a4d3eae&quot;&gt;&lt;header&gt;&lt;p class=&quot;page-description&quot;&gt;&lt;/p&gt;&lt;/header&gt;&lt;div class=&quot;page-body Tistory&quot;&gt;&lt;p class=&quot;&quot; id=&quot;ce036c20-f804-4193-a092-606832370af4&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;ed9829fe-2f5d-4d79-be39-eea4d7c36014&quot;&gt;
&lt;/p&gt;&lt;h1 class=&quot;&quot; id=&quot;b0598565-5ee2-4105-ad0f-c75dd621601c&quot;&gt;BackPressure &lt;/h1&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;de6236b2-721f-4cb7-a921-0ffaf4c0a632&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;업스트림 생산자가 다운스트림 소비자에게 스트림을 emit하는데, 소비자가 emit속도를 맞춰 방출할 수 없을 때 발생 ( 처리속도 고려 없이 emit )&lt;/li&gt;&lt;/ul&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;4434932f-c838-4c03-885d-3f7dc72acf6c&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;메모리 사용 및 성능 문제가 발생&lt;/li&gt;&lt;/ul&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;473b83df-d062-4124-b9d6-b6393b6a7ce7&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;해결하기 위한건? → buffer&lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;4c83f769-7a03-4839-adaf-bf25d6cfda5c&quot;&gt;
&lt;/p&gt;&lt;h1 class=&quot;&quot; id=&quot;970bcc3f-1ccf-4d3a-85fe-6d52dc0b8230&quot;&gt;Buffer&lt;/h1&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;ab0b6489-0837-4ede-a8ba-4628572f5e35&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;아래와 같이 적용하면 됩니다&lt;/li&gt;&lt;/ul&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;50cce8c4-44b5-4ef9-aced-871cc9cdd95f&quot;&gt;&lt;code&gt;lifecycleScope.launch {
            flowOf(&quot;A&quot;,&quot;B&quot;,&quot;C&quot;,&quot;D&quot;,&quot;E&quot;)
                .onEach { println(&quot;Wizard emits: $it&quot;) }
                .buffer()
                .collect {
                    println(&quot;Lady collects: $it&quot;)
                    delay(2000)
                }
        }&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;c8d966ee-4574-454c-b198-51e73d15e43a&quot;&gt;
&lt;/p&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;16b27847-7a29-4999-8bef-2e3478beac2d&quot; start=&quot;1&quot; type=&quot;1&quot;&gt;&lt;li&gt;요것이&lt;/li&gt;&lt;/ol&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;a127062e-434f-42db-b852-f0f41d4fec00&quot;&gt;&lt;code&gt;Wizard emits A
Lady collects A
Wizard emits B
Lady collects B
Wizard emits C
Lady collects C
Wizard emits D
Lady collects D
Wizard emits E
Lady collects E&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;e5dbb263-ecb2-4ec3-8613-cecebbc2f747&quot;&gt;
&lt;/p&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;7a37c9aa-c887-43c9-b445-0e6189c65328&quot; start=&quot;2&quot; type=&quot;1&quot;&gt;&lt;li&gt;이렇게 바뀝니다&lt;/li&gt;&lt;/ol&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;ded3427f-3a7b-4c09-ac8d-ccc37ceee6b2&quot;&gt;&lt;code&gt;Wizard emits A
Wizard emits B
Wizard emits C
Wizard emits D
Wizard emits E
Lady collects A
Lady collects B
Lady collects C
Lady collects D
Lady collects E&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;1f09c905-d4fd-4a77-986d-a968a4cc6c52&quot;&gt;
&lt;/p&gt;&lt;h3 class=&quot;&quot; id=&quot;c0092622-e1b8-4427-8c00-c37e8741d70e&quot;&gt;효과&lt;/h3&gt;&lt;p class=&quot;&quot; id=&quot;d9015743-9f5b-46cd-bea5-61e63e16f5c8&quot;&gt;원래, flow를 만들고, 작업을 수행하고, emit하고, collect를 하는 전체 프로세스가 단일 코루틴에서 발생함. buffer는 단일 코루틴을 두 개로 분할함&lt;/p&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;eed73b5a-3585-437a-bbf0-45d0252f8e7c&quot;&gt;&lt;code&gt;flowOf(&quot;A&quot;,&quot;B&quot;,&quot;C&quot;,&quot;D&quot;,&quot;E&quot;)
                .onEach { println(&quot;Wizard emits: $it&quot;) } // Wizard CR
                .buffer()
                .collect {                                // Lady CR
                    println(&quot;Lady collects: $it&quot;)
                    delay(2000)
                }&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;f42ba30d-36f2-4472-8344-b55a4d2f8b76&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;110f705b-6679-4d4e-9c39-de3862c27a2d&quot;&gt;둘은 연결된 채널이 존재하고, 아래와 같이, 너무 많은 데이터 스트림이 한 번에 전달되면, 다운스트림에서 이걸 소화할 수 있을 때 까지 emit을 중단함&lt;/p&gt;&lt;figure class=&quot;image&quot; id=&quot;db3f87c2-3c3d-494f-8a60-a21f5c428958&quot; style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/2sp0N/btsBcaS6r1d/2ONq1vgNCsV27Td3bG0z4k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2sp0N/btsBcaS6r1d/2ONq1vgNCsV27Td3bG0z4k/img.png&quot; style=&quot;width:1152px&quot;/&gt;&lt;/a&gt;&lt;/figure&gt;&lt;p class=&quot;&quot; id=&quot;ef49bf3f-718f-4eee-88bb-f125b8b45ca0&quot;&gt;
&lt;/p&gt;&lt;h1 class=&quot;&quot; id=&quot;43a022a7-d871-4015-b1bb-284dd46beab7&quot;&gt;버퍼함수 인수&lt;/h1&gt;&lt;p class=&quot;&quot; id=&quot;296c22b8-499c-40ed-be9b-67eb31b5ecac&quot;&gt;버퍼 함수는 두 가지 인자를 받는다&lt;/p&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;014b3f0e-d61d-4137-bbb6-a93993d3f1ef&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;&lt;em&gt;capacity: Integer type&lt;/em&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;18fe46ff-9360-4ca3-a6fa-6c66c4d7da1c&quot;&gt;&lt;li style=&quot;list-style-type:circle&quot;&gt;버퍼가 보유할 수 있는 최대 값 수&lt;/li&gt;&lt;/ul&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;af243ea1-8048-4a21-a7aa-944e03437c19&quot;&gt;&lt;li style=&quot;list-style-type:circle&quot;&gt;초과하면 일시 중지됨&lt;/li&gt;&lt;/ul&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;23e4b310-55a1-4e9c-89d8-0e2f245c9b9b&quot;&gt;&lt;li style=&quot;list-style-type:circle&quot;&gt;불규칙한 패턴으로 데이터를 수신하는 앱을 개발하는 경우 사용~&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;f7d2a8a8-aa5f-4202-af42-e3af14e35060&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;&lt;em&gt;onBufferOverflow: BufferOverflow class&lt;/em&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;88941f0d-ae4a-459b-bbc9-09d2fdc95a07&quot;&gt;&lt;li style=&quot;list-style-type:circle&quot;&gt;용량을 초과하게 오는 데이터를 처리하는 로직&lt;ol class=&quot;numbered-list&quot; id=&quot;bcd0e8d8-c574-48c8-b61f-1762d44f0152&quot; start=&quot;1&quot; type=&quot;1&quot;&gt;&lt;li&gt;BufferOverflow.SUSPEND(기본값) : 버퍼에 공간이 생길 때 까지, 추가 일시 중지됨&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;6f7f1593-3b82-4baf-b17c-628ab85d05fe&quot; start=&quot;2&quot; type=&quot;1&quot;&gt;&lt;li&gt;BufferOverflow.DROP_OLDEST : 버퍼의 가장 오래된 문자를 삭제, 새로운 문자 허용&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;d1ee747e-30a5-4645-a7d0-2b256e0c14e6&quot; start=&quot;3&quot; type=&quot;1&quot;&gt;&lt;li&gt;BufferOverflow.DROP_LATEST : 버퍼의 최신 문자를 삭제하고 새 문자를 허용 &lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;7733f5c2-f54d-4895-91f7-f2819108e827&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;cefc8052-0fcd-410a-8ed5-55be329d65a3&quot;&gt;
&lt;/p&gt;&lt;html&gt;&lt;body&gt;&lt;br/&gt;&lt;p class=&quot;&quot;&gt;Uploaded by &lt;mark class=&quot;highlight-orange&quot;&gt;&lt;a href=&quot;https://github.com/jmjeon94/N2T&quot;&gt;N2T&lt;/a&gt;&lt;/mark&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;&lt;/div&gt;&lt;/article&gt;&lt;span class=&quot;sans&quot; style=&quot;font-size:14px;padding-top:2em&quot;&gt;&lt;/span&gt;&lt;/body&gt;</description>
      <category>개발/Kotlin</category>
      <category>backpressure</category>
      <category>Buffer</category>
      <category>Coroutine</category>
      <category>Coroutine Flow</category>
      <category>flow</category>
      <category>Kotlin</category>
      <category>코루틴</category>
      <category>코틀린</category>
      <author>이도일</author>
      <guid isPermaLink="true">https://dev-soyeon.tistory.com/97</guid>
      <comments>https://dev-soyeon.tistory.com/97#entry97comment</comments>
      <pubDate>Thu, 30 Nov 2023 11:01:20 +0900</pubDate>
    </item>
    <item>
      <title>Android 에서 TDD를 습관화하기</title>
      <link>https://dev-soyeon.tistory.com/96</link>
      <description>&lt;body&gt;&lt;html&gt;&lt;head&gt;&lt;link href=&quot;//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.2/styles/atom-one-dark.min.css&quot; rel=&quot;stylesheet&quot;/&gt;
&lt;script src=&quot;//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.2/highlight.min.js&quot;&gt;&lt;/script&gt;
&lt;script&gt;hljs.initHighlightingOnLoad();&lt;/script&gt;&lt;/head&gt;&lt;/html&gt;&lt;link href=&quot;https://rawcdn.githack.com/ppuep94/n2t/5ef4dc01e9d6336341e9ab95bb71672f9d3a3dc9/assets/css/style2.css&quot; rel=&quot;stylesheet&quot;/&gt;&lt;article class=&quot;page sans Notion&quot; id=&quot;b98b97a2-1e0c-4a44-97fa-508f02cb1dfd&quot;&gt;&lt;header&gt;&lt;p class=&quot;page-description&quot;&gt;&lt;/p&gt;&lt;/header&gt;&lt;div class=&quot;page-body Tistory&quot;&gt;&lt;p class=&quot;&quot; id=&quot;ea88005e-f433-467a-a477-2a0c0c66eb46&quot;&gt;
&lt;/p&gt;&lt;h2 class=&quot;&quot; id=&quot;3f6f1589-842c-49c4-ad44-fde22956b819&quot;&gt;TDD?&lt;/h2&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;8e36dfc5-4b65-4064-bb25-8f30aad9346b&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;Test Driven Development&lt;/li&gt;&lt;/ul&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;97cafd38-8191-4cce-ae57-eaf6407ba08b&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;sw의 개발 전에, 테스트 사례로 제시되는 요구사항에 의존하는 개발 프로세스&lt;/li&gt;&lt;/ul&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;e3125833-d8b5-4ad9-8e49-6518ed69b691&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;가장 쉽고 빠르고, 신뢰 가능한 방법은 Red, Green, Refactor로 구성하는 방법! &lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;ef5e9807-f65a-42ac-96c9-07c62c13033b&quot;&gt;
&lt;/p&gt;&lt;h2 class=&quot;&quot; id=&quot;ffd101c7-8dff-403e-a15e-a35590dadb4b&quot;&gt;테스트 라이브러리&lt;/h2&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;b49cf4ee-a200-4070-aa1c-203586c18a03&quot; start=&quot;1&quot; type=&quot;1&quot;&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/google/truth&quot;&gt;Truth&lt;/a&gt; — value를 asserting하기 위한 라이브러리&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;8e2f34f7-b07b-4e02-ac3b-ff1d93b18e12&quot; start=&quot;2&quot; type=&quot;1&quot;&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/cashapp/turbine&quot;&gt;Turbine&lt;/a&gt; — &lt;a href=&quot;https://kotlinlang.org/docs/flow.html&quot;&gt;Flow&lt;/a&gt;에서 받은 value를 asserting하기 위한 라이브러리 &lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;b3ece45d-260d-4cbb-968c-2fe48b31b2c3&quot; start=&quot;3&quot; type=&quot;1&quot;&gt;&lt;li&gt;&lt;a href=&quot;https://site.mockito.org/&quot;&gt;Mockito&lt;/a&gt; — mock데이터를 위한 라이브러리&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;fc187432-d433-4144-a923-72ce01917277&quot; start=&quot;4&quot; type=&quot;1&quot;&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/junit-team/junit4&quot;&gt;JUnit4&lt;/a&gt; — java 기본 유닛 테스트 프레임워크&lt;/li&gt;&lt;/ol&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;c9a1b4e6-f905-4902-9ddc-702aae35928e&quot; start=&quot;5&quot; type=&quot;1&quot;&gt;&lt;li&gt;&lt;a href=&quot;https://junit.org/junit5/&quot;&gt;JUnit5&lt;/a&gt; — Java 단위 테스트 프레임워크 &lt;/li&gt;&lt;/ol&gt;&lt;p class=&quot;&quot; id=&quot;550d6af7-837e-4e7b-aec3-6208cad48e11&quot;&gt;→ 알아서 추가하시오 &lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;f35eed55-b2cb-4fab-aed8-40c5743cf3a6&quot;&gt;
&lt;/p&gt;&lt;h2 class=&quot;&quot; id=&quot;31a9488e-8cc4-4670-8723-e8607028d145&quot;&gt;예제&lt;/h2&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;2be55fe2-7715-4e72-936f-f056a11a24d8&quot;&gt;&lt;code&gt;@AndroidEntryPoint
class ContactsFragment : Fragment() {
    // Let's pretend this is a class responsible to retrieve data from API
    @Inject
    lateinit var megaApi: MegaApi

    private var _binding: FragmentContactsBinding? = null
    private val binding get() = _binding!!

    private var mAdapter: ContactsAdapter? = null

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?,
    ): View {
        _binding = FragmentContactsBinding.inflate(layoutInflater)
        binding.recyclerView.apply {
            addItemDecoration(SimpleDividerItemDecoration(requireContext()))
            layoutManager = LinearLayoutManager(requireContext())
            itemAnimator = DefaultItemAnimator()
        }
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        fetchContacts()
    }

    override fun onDestroyView() {
        _binding = null
        super.onDestroyView()
    }

    private fun fetchContacts() = lifecycleScope.launch(Dispatchers.Main) {
        binding.loadingLayout.isVisible = true
        val contacts = try {
            withContext(Dispatchers.IO) { 
                megaApi.getContacts() 
            }
        } catch (e: Exception) {
            showErrorLayout()
            return
        }
        updateContactsUI(contacts)
    }

    private fun updateContactsUI(contacts: List&amp;lt;Contact&amp;gt;?) {
        when {
            contacts.isNullOrEmpty() -&amp;gt; showEmptyLayout()
            else -&amp;gt; {
                if (mAdapter == null) {
                    mAdapter = ContactsAdapter(
                        requireActivity(),
                        this,
                        contacts
                    )
                }
                
                binding.recyclerView.adapter = mAdapter
                binding.recyclerView.isVisible = true
                binding.loadingLayout.isVisible = false
                binding.emptyLayout.isVisible = false
                binding.errorLayout.isVisible = false
            }
        }
    }

    private fun showEmptyLayout() {
        binding.emptyLayout.isVisible = true
        binding.recyclerView.isVisible = false
        binding.errorLayout.isVisible = false
        binding.loadingLayout.isVisible = false
    }

    private fun showErrorLayout() {
        binding.errorLayout.isVisible = true
        binding.recyclerView.isVisible = false
        binding.emptyLayout.isVisible = false
        binding.loadingLayout.isVisible = false
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;33dc75c0-9219-46a8-b5a2-a2f7e1b5b07b&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;이걸로 compose 적용 + tdd 적용할 예정&lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;af9e405e-5349-4ef4-8ef7-8970391fe82b&quot;&gt;
&lt;/p&gt;&lt;h3 class=&quot;&quot; id=&quot;94dccd20-9a58-4d5c-b170-3e2346a0cb6c&quot;&gt;필요 클래스&lt;/h3&gt;&lt;p class=&quot;&quot; id=&quot;77c6e3c9-87ef-4600-aebd-253862441f41&quot;&gt;&lt;code&gt;Contact.kt&lt;/code&gt; — contacts list의 각 contact에 대한 데이터 클래스&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;5bc834c5-0813-4ab6-942d-2fa6937469c7&quot;&gt;&lt;code&gt;ContactsViewModel.kt&lt;/code&gt;— 뷰에 대한 뷰 모델&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;f8a1702d-16a3-4fa6-99d6-28bd2e3b5248&quot;&gt;&lt;code&gt;ContactsRoute.kt&lt;/code&gt;— Jetpack Compose의 경로, 화면, 보기&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;be23e912-b894-4e45-8531-07fceb416fc8&quot;&gt;&lt;code&gt;ContactsRepository.kt&lt;/code&gt;— 데이터를 검색하는 저장소&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;4363edb4-8cb2-4395-a54c-68ec53014bf9&quot;&gt;&lt;code&gt;UIState.kt&lt;/code&gt;&lt;code&gt;ContactsScreen&lt;/code&gt;— 뷰 상태를 유지할 UI 상태&lt;/p&gt;&lt;h3 class=&quot;&quot; id=&quot;435e659d-6cc1-4788-b85a-8ffc11ecc539&quot;&gt;테스트 클래스&lt;/h3&gt;&lt;p class=&quot;&quot; id=&quot;abe7f27a-ad6e-4b40-9374-8b6430357105&quot;&gt;&lt;code&gt;ContactsViewModelTest.kt&lt;/code&gt;— &lt;code&gt;ContactsViewModel&lt;/code&gt; 의 테스트 클래스&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;263b90ac-5d8d-4fab-9c91-950e3ba31243&quot;&gt;&lt;code&gt;ContactsScreenTest.kt&lt;/code&gt;— &lt;code&gt;ContactsScreen&lt;/code&gt; 의 테스트 클래스&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;68761d4d-583a-4db2-8d55-46d58d461e5e&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;e7525f12-6fd0-4a41-917c-87083ec7a65e&quot;&gt;
&lt;/p&gt;&lt;h3 class=&quot;&quot; id=&quot;8465f878-180c-4eac-9bf1-ae90ffb16ba7&quot;&gt;1단계. viewModel의 테스트 클래스 생성하기&lt;/h3&gt;&lt;p class=&quot;&quot; id=&quot;51aeadf1-b0d3-47c6-9fc1-82dc36d6a378&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;f668ae01-66d8-4043-9a34-be78927b547b&quot;&gt;아래와 같이 테스트 클래스를 &lt;strong&gt;먼저 &lt;/strong&gt;생성해준다&lt;/p&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;9e50a645-86dd-4226-a28d-393b9a97c91b&quot;&gt;&lt;code&gt;class ContactsViewModelTest {}&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;4b095c50-ada3-4e1d-8e93-aecd0e780cdc&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;09b813ff-3111-4be7-a019-c7bfabc51d6a&quot;&gt;그 다음, 테스트의 명세를 작성해준다&lt;/p&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;e8da69c9-a2e9-4e62-bbad-9e48b5f61a71&quot;&gt;&lt;code&gt;fun `ViewModel이 처음 로드될 때 loading indicator의 가시 상태 가 true 인지 테스트합니다.`() {}&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;229c8134-4d8c-4c50-b28e-b3b0817507c9&quot;&gt;
&lt;/p&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;0def38a6-6e96-4d8d-8176-0cbba07e65a1&quot;&gt;&lt;code&gt;fun ` 연락처 가져오기가 성공적으로 완료 되면 loading indicator 가시 상태가 false 인지 테스트합니다.`  ()  {}
 fun ` 연락처 가져오기가 오류를 반환할 때 loading indicator 가시 상태가 false 인지 테스트합니다  ` ()  {}&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;a4e6ca02-7ff0-4d79-b95e-9ad5ea4da45b&quot;&gt;
&lt;/p&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;bf69e25e-697b-43ed-9789-c5d08cf76e58&quot;&gt;&lt;code&gt;fun ` 연락처 가져오기 성공 시 연락처 데이터가 업데이트되어야 하는지 테스트 ` () {}
 fun ` 연락처 가져오기 에서 오류가 발생 하면 오류 레이아웃 가시성이 true 인지  테스트` ()  {}&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;b233d8e2-a4cd-482b-b8f2-5af6dbce659c&quot;&gt;
&lt;/p&gt;&lt;h3 class=&quot;&quot; id=&quot;637af57e-ce81-484d-a3e4-c3acd2cfe1a7&quot;&gt;2단계. AAA(&lt;strong&gt;Arrange, Action, Assert)&lt;/strong&gt;&lt;/h3&gt;&lt;p class=&quot;&quot; id=&quot;7fc658e8-21fc-480a-975d-0a1b401851cd&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;7c762a66-156f-476a-9139-f3c8834cd540&quot;&gt;각 테스트를 3개의 섹션으로 나누기&lt;/p&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;1badbe7b-aac8-4dd9-b44a-95d8ad823f9b&quot;&gt;&lt;code&gt;@Test 
fun ` ViewModel이 처음 로드될 때 로드 표시기 가시성 상태 가  true 인지 테스트합니다.`  () { 
		// Arrange
    // Act
    // Assert
 }&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;7f23b218-7268-4f3e-b74f-a1da03b9a99a&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;edef9323-6595-4075-a71f-cc4a6ae1d679&quot;&gt;ui상태 클래스 안만들었으면 지금 만들기&lt;/p&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;635e7200-697b-4a8f-a33a-ecbf95f560fb&quot;&gt;&lt;code&gt;data class UIState(
    val isLoading: Boolean = false,
    val isError: Boolean = false,
    val contacts: List&amp;lt;Contact&amp;gt; = emptyList()
)&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;f42a35a6-5faa-4bc8-8e5e-03cac3d9e84b&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;3fb20036-d7b6-40c1-a3d1-864175be5e4a&quot;&gt;Repository도 만들기&lt;/p&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;595508a5-fb8f-49db-b12e-c46aae99075c&quot;&gt;&lt;code&gt;class ContactsRepository @Inject constructor(
    private val megaApi: MegaApiAndroid
) {
    suspend fun getContacts(): List&amp;lt;Contact&amp;gt; = withContext(Dispatchers.IO) {
        megaApi.getContacts()
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;0cbb34e2-d33c-4862-b4d0-198db45f6c6b&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;7d989afb-b19a-42d7-a8fc-a8b2c8c197d7&quot;&gt;연락처 데이터 받을 데이터 클래스(모델) 도 만들기 &lt;/p&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;773956a9-4428-48ea-a4cb-2e89b6ba6539&quot;&gt;&lt;code&gt;data class Contact(
    val name: String,
    val email: String,
    val phone: String
)&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;2a478ef2-b9f5-4575-85f2-a96701ae0303&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;9091b4f5-2601-4a2e-879b-07de1972cedf&quot;&gt;&lt;strong&gt;2.1단계 - Assert&lt;/strong&gt;&lt;/p&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;ec917422-cb14-4d3e-bd70-828da4bcf3c8&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;최종 목표&lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;679a6d76-c6c4-4ce4-a38c-a2541145db06&quot;&gt;&lt;strong&gt;2.2단계 — Act&lt;/strong&gt;&lt;/p&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;a45ad45e-483b-4b88-afe1-041c6ca2dadf&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;기본적으로 결과를 달성하는 데 필요한 조치&lt;/li&gt;&lt;/ul&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;300a6f6b-cc6d-4eb0-8534-8cd6b3684e41&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;여기에서 메서드를 호출&lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;699bcb9e-f1d5-4d5c-bab9-90703959b757&quot;&gt;&lt;strong&gt;2.3단계 - Arrange&lt;/strong&gt;&lt;/p&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;83d6d32b-b4e8-4e87-b475-317ee78015d3&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;전제 조건&lt;/li&gt;&lt;/ul&gt;&lt;ul class=&quot;bulleted-list&quot; id=&quot;c1ae01d1-fafd-4de2-9a6d-c89b6b0c03da&quot;&gt;&lt;li style=&quot;list-style-type:disc&quot;&gt;몇몇 테스트 케이스에서 viewmodel만 필요하므로 잠시 init에선 비워둠&lt;/li&gt;&lt;/ul&gt;&lt;p class=&quot;&quot; id=&quot;fda45b77-1422-4f37-8c22-7e0a5125b4a3&quot;&gt;
&lt;/p&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;dc5a1a9e-aff0-4136-bd44-dd2059ba41b5&quot;&gt;&lt;code&gt;@Test
fun `ViewModel이 처음 로드될 때 loading indicator의 가시 상태 가 true 인지 테스트합니다.`() {}    // Arrange
    
    // Act
    initTestClass()
    
    // Assert
    Truth.assertThat(state.isLoading).isTrue()
}

@Test
fun ` 연락처 가져오기가 성공적으로 완료 되면 loading indicator 가시 상태가 false 인지 테스트합니다.`  ()  {}
    // Arrange
    val contacts: List&amp;lt;Contact&amp;gt; = mock()
    whenever(contactsRepository.getContacts()).thenReturn(contacts)
    
    // Act
    initTestClass()
    
    // Assert
    Truth.assertThat(state.isLoading).isFalse()
}

@Test
 fun ` 연락처 가져오기가 오류를 반환할 때 loading indicator 가시 상태가 false 인지 테스트합니다  ` ()  {}    // Arrange
    whenever(contactsRepository.getCurrengetContactstUserContacts()).thenThrow(RuntimeException())
    // Act
    initTestClass()
    
    // Assert
    Truth.assertThat(state.isLoading).isFalse()
}

@Test
fun ` 연락처 가져오기 성공 시 연락처 데이터가 업데이트되어야 하는지 테스트 ` () {}
    // Arrange
    val contacts: List&amp;lt;Contact&amp;gt; = mock()
    whenever(contactsRepository.getContacts()).thenReturn(contacts)
    // Act
    initTestClass()
    
    // Assert
    verify(contactsRepository).getContacts()
    Truth.assertThat(state.isLoading).isFalse()
    Truth.assertThat(state.contacts).isEqualTo(contacts)
}

@Test
 fun ` 연락처 가져오기 에서 오류가 발생 하면 오류 레이아웃 가시성이 true 인지  테스트` ()  {}    // Arrange
    whenever(contactsRepository.getContacts()).thenThrow(RuntimeException())
    // Act
    initTestClass()
    
    // Assert
    Truth.assertThat(state.isError).isTrue()
}&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;434a9558-0aba-4ca5-8b73-f5a40025d394&quot;&gt;
&lt;/p&gt;&lt;h3 class=&quot;&quot; id=&quot;026f1afe-5535-4416-8778-30145d7577a1&quot;&gt;3단계. ViewModel생성&lt;/h3&gt;&lt;p class=&quot;&quot; id=&quot;04b10f77-4cb7-4d8f-b810-1e86d1ab0f54&quot;&gt;이전 단계에서 정의한 각 테스트 케이스를 구현하고, 이를 뷰모델에 적용하자. 모든 테스트 케이스가 구현되었는지 확인하고, 결과를 추적하자.&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;b07088a6-5933-41de-a155-d0fd4041881f&quot;&gt;아래는 연락처를 가져오는 테스트의 구현 부분만 비운 코드다.&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;ca6fce5d-9c54-4260-abbb-69ba2c3f23e5&quot;&gt;
&lt;/p&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;549a8803-8982-41d6-9c36-96f9138de414&quot;&gt;&lt;code&gt;@HiltViewModel
class ContactsViewModel @Inject constructor(
    private val contactsRepository: ContactsRepository
) {
    private val _uiState = MutableStateFlow(UIState())
    val uiState = _uiState.asStateFlow()

    init {
        fetchContacts()
    }

    private fun fetchContacts() = viewModelScope.launch {
        // TODO
    }&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;fe4b41c5-18de-4df5-a0d4-07917c525bb8&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;4207b29e-d7a4-46dd-8abe-35435c6b1458&quot;&gt;이제 테스트 클래스로 돌아가서, 컴파일이 가능한지 확인하고, 모든 테스트가 실패했는지도 확인하자. &lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;0aff65c5-05cd-49f7-bd81-5cd26678ffa7&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;883e9f78-73e6-4657-815c-588ac97c9f12&quot;&gt;Red / Green / Refactor중 Red가 가장 중요하다.!&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;f4c37e2b-26a1-4e10-ad32-d45baaf7f284&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;d1f77f4f-1318-4674-8ce9-5093135f4402&quot;&gt;코드는 아래와 같이 완성되어야한다&lt;/p&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;ff4faeed-7f24-46ef-bece-e56dfad02709&quot;&gt;&lt;code&gt;@OptIn(ExperimentalCoroutinesApi::class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class ContactsViewModelTest {
    private val contactsRepository: ContactsRepository = mock()
    private lateinit var underTest: ContactsViewModel
    
    @BeforeAll
    fun init() {
        Dispatchers.setMain(UnconfinedTestDispatcher())
    }

    @AfterAll
    fun tearDown() {
        Dispatchers.resetMain()
    }

    @BeforeEach
    fun setup() {
        reset(contactsRepository)
    }
    
    private fun initTestClass() {
        underTest = ContactsViewModel(contactsRepository)
    }
    
    @Test
		fun `ViewModel이 처음 로드될 때 loading indicator의 가시 상태 가 true 인지 테스트합니다.`() {}    // Arrange
        // Arrange
        
        // Act
        initTestClass()
        
        // Assert
        underTest.uiState.test {
            val state = awaitItem()
            Truth.assertThat(state.isLoading).isTrue()
        }
    }
    
    @Test
		fun ` 연락처 가져오기가 성공적으로 완료 되면 loading indicator 가시 상태가 false 인지 테스트합니다.`  ()  {}
        // Arrange
        val contacts: List&amp;lt;Contact&amp;gt; = mock()
        whenever(contactsRepository.getContacts()).thenReturn(contacts)
        
        // Act
        initTestClass()
        
        // Assert
        underTest.uiState.test {
            awaitItem()
            val state = awaitItem()
            Truth.assertThat(state.isLoading).isFalse()
        }
    }
    
    @Test
		 fun ` 연락처 가져오기가 오류를 반환할 때 loading indicator 가시 상태가 false 인지 테스트합니다  ` ()  {}    // Arrange
        // Arrange
        whenever(contactsRepository.getContacts()).thenThrow(RuntimeException())
    
        // Act
        initTestClass()
        
        // Assert
        underTest.uiState.test {
            awaitItem()
            val state = awaitItem()
            Truth.assertThat(state.isLoading).isFalse()
        }
    }
    
    @Test
		fun ` 연락처 가져오기 성공 시 연락처 데이터가 업데이트되어야 하는지 테스트 ` () {}
        // Arrange
        val contacts: List&amp;lt;Contact&amp;gt; = mock()
        whenever(contactsRepository.getContacts()).thenReturn(contacts)
    
        // Act
        initTestClass()
        
        // Assert
        verify(contactsRepository).getContacts()
        underTest.uiState.test {
            awaitItem()
            val state = awaitItem()
            Truth.assertThat(state.isLoading).isFalse()
            Truth.assertThat(state.contacts).isEqualTo(contacts)
        }
    }
    
    @Test
	 fun ` 연락처 가져오기 에서 오류가 발생 하면 오류 레이아웃 가시성이 true 인지  테스트` ()  {}    // Arrange
        // Arrange
        whenever(contactsRepository.getContacts()).thenThrow(RuntimeException())
    
        // Act
        initTestClass()
    
        // Assert
        underTest.uiState.test {
            awaitItem()
            val state = awaitItem()
            Truth.assertThat(state.isError).isTrue()
        }
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;90a9a0d8-6a25-41de-a641-d32f0d8a640f&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;1614a491-0479-49ab-ad43-dc512b11e6c7&quot;&gt;
&lt;/p&gt;&lt;h3 class=&quot;&quot; id=&quot;09581a0f-8d17-44f0-84fd-31a2cb9711fc&quot;&gt;&lt;strong&gt;4단계 - 테스트는 실패하고, 컴파일은 가능한지 확인(RED)&lt;/strong&gt;&lt;/h3&gt;&lt;p class=&quot;&quot; id=&quot;5349dcca-0af4-47af-9af4-d4e1293752cd&quot;&gt;이 시점에서 컴파일은 가능해야한다. 이제 테스트를 통과하도록 만들 차례다.&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;d291324d-deb9-44b9-ae21-7133daa669da&quot;&gt;
&lt;/p&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;5be77dff-258b-48e0-8945-dc35dd2c1c7b&quot; start=&quot;1&quot; type=&quot;1&quot;&gt;&lt;li&gt;loading indicator는 viewModel이 처음 로드될 때 나타나야하므로, init ViewModel부분에서 이를 true로 설정한다.&lt;/li&gt;&lt;/ol&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;cf2d74af-a8ee-4276-a659-5d6cfd20115c&quot;&gt;&lt;code&gt;@HiltViewModel
class ContactsViewModel @Inject constructor(
    private val contactsRepository: ContactsRepository
) {
    private val _uiState = MutableStateFlow(UIState())
    val uiState = _uiState.asStateFlow()

    init {
        // test that loading indicator visibility state is true when ViewModel first load
        _uiState.update { it.copy(isLoading = true) }
        fetchContacts()
    }

    private fun fetchContacts() = viewModelScope.launch {
            // TODO
        }
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;a3dc1164-85dc-472e-bf6a-32ae2755a6b9&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;df9ee91f-542b-4635-bd17-b30e62f0166d&quot;&gt;테스트 해보기&lt;/p&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;659f5508-b48f-4f9d-9f35-39977f0ae069&quot;&gt;&lt;code&gt;@Test 
fun `ViewModel이 처음 로드될 때 loading indicator의 가시 상태가 true 인지 테스트합니다.`() {}    // Arrange
    // 배열 
    
    // Act
     initTestClass() 
    
    // Assert
     underTest.uiState.test { 
        val state = waitItem() 
        Truth.assertThat( state.isLoading).isTrue() 
    } 
}&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;10843d68-df5c-482f-a491-083f36ff57ac&quot;&gt;
&lt;/p&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;874ef98c-f73c-4a8f-83c2-d09f87c4795e&quot; start=&quot;2&quot; type=&quot;1&quot;&gt;&lt;li&gt;다음으로는, API 로드가 성공적으로 끝나면 loading indicator가 사라져야 한다는것&lt;/li&gt;&lt;/ol&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;a6a13bf7-31d2-4238-b2bf-96045e0c6d7c&quot;&gt;&lt;code&gt;private  fun  fetchContacts () = viewModelScope.launch { 
    runCatching { 
        // 저장소에서 데이터 가져오기
         contactRepository.getContacts() 
    }.onSuccess { 
        // 연락처 가져오기 성공 시 로딩 표시기 가시성 상태가 false인지 테스트
         _uiState.update { 
            it.copy(isLoading = false ) 
        } 
    } 
}&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;da798c73-1845-49db-9b74-95a090b5cb7f&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;8c0d8c0e-fb27-4b9d-b188-a49446a845e9&quot;&gt;테스트&lt;/p&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;a792e2b1-231b-4d14-8c75-d504e5c6a8df&quot;&gt;&lt;code&gt;@Test 
fun `ViewModel이 처음 로드될 때 loading indicator의 가시 상태가 true 인지 테스트합니다.`() {}    // Arrange
    ... 
} 

@Test 
fun ` 연락처 가져오기가 성공적으로 완료 되면 loading indicator 가시 상태가 false 인지 테스트합니다.`  ()  {}
    // 
    val 연락처 정렬 : List&amp;lt;Contact&amp;gt; = mock() 
    when(contactsRepository.getContacts()).thenReturn(contacts) 
    
    // Act
     initTestClass() 
    
    // Assert
     underTest.uiState.test { 
        waitItem() 
        val state = waitItem() 
        Truth .assertThat(state.isLoading).isFalse() 
    } 
}&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;69137bd1-fea6-4c50-b06f-499659d944bf&quot;&gt;
&lt;/p&gt;&lt;ol class=&quot;numbered-list&quot; id=&quot;db9d4d07-2486-433a-92ed-68b9e6bf89d7&quot; start=&quot;3&quot; type=&quot;1&quot;&gt;&lt;li&gt;API호출에서 오류가 있다면 로딩 상태 false 인지 확인&lt;/li&gt;&lt;/ol&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;d8b00899-57a9-4017-b114-2c673f210224&quot;&gt;&lt;code&gt;private  fun  fetchContacts () = viewModelScope.launch { 
    runCatching { 
        // 저장소에서 데이터 가져오기
         contactRepository.getContacts() 
    }.onSuccess { 
        // 연락처 가져오기에 성공하면 로드 표시기 가시성 상태가 false인지 테스트
         ... 
    }.onFailure { 
        // 연락처 가져오기 오류가 반환될 때 로드 표시기 가시성 상태가 false인지 테스트합니다.
         _uiState.update { it.copy(isLoading = false ) } 
    } 
}&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;eae795a5-d4a9-4aca-948b-fa4f29a05c74&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;0f60178e-e800-46d5-b7dc-95681c928172&quot;&gt;테스트&lt;/p&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;db000de8-ac51-4901-8033-5901e840e7cf&quot;&gt;&lt;code&gt;  @Test
		 fun ` 연락처 가져오기가 오류를 반환할 때 loading indicator 가시 상태가 false 인지 테스트합니다  ` ()  {}    // Arrange
        // Arrange
        whenever(contactsRepository.getContacts()).thenThrow(RuntimeException())
    
        // Act
        initTestClass()
        
        // Assert
        underTest.uiState.test {
            awaitItem()
            val state = awaitItem()
            Truth.assertThat(state.isLoading).isFalse()
        }
    }
    &lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;10a037eb-2328-4c2f-b992-1e80440d96ff&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;b0bbd584-4ff6-4595-adf1-831532dfeabc&quot;&gt;API가 제대로 오면 연락처 데이터 상태가 업데이트 되는지 확인&lt;/p&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;81dc4551-a51b-402f-89b5-dd84b9897722&quot;&gt;&lt;code&gt;private  fun  fetchContacts () = viewModelScope.launch { 
    runCatching { 
        // 저장소에서 데이터 가져오기
         contactRepository.getContacts() 
    }.onSuccess { 
        // 연락처 가져오기가 성공하면 로드 표시기 가시성 상태가 false인지 테스트 
        // 연락처 데이터를 업데이트해야 하는지 테스트 연락처 가져오기 성공 시
         _uiState.update { it.copy(isLoading = false , contact = currentContacts) } 
    }.onFailure { 
        // 연락처 가져오기 오류가 반환될 때 로드 표시기 가시성 상태가 false인지 테스트
         _uiState.update { it.copy(isLoading = false) } 
    }&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;a348e9ba-9195-46ba-8006-4264a8ca6262&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;8c10fcaf-76d3-4235-842b-2155562561e1&quot;&gt;테스트&lt;/p&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;6f77d444-b8f9-46ba-8096-a7216af6c6fd&quot;&gt;&lt;code&gt; 
    @Test
		fun ` 연락처 가져오기 성공 시 연락처 데이터가 업데이트되어야 하는지 테스트 ` () {}
        // Arrange
        val contacts: List&amp;lt;Contact&amp;gt; = mock()
        whenever(contactsRepository.getContacts()).thenReturn(contacts)
    
        // Act
        initTestClass()
        
        // Assert
        verify(contactsRepository).getContacts()
        underTest.uiState.test {
            awaitItem()
            val state = awaitItem()
            Truth.assertThat(state.isLoading).isFalse()
            Truth.assertThat(state.contacts).isEqualTo(contacts)
        }
    }
    &lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;ef0ce20f-c02c-4eb0-99c9-ff194c451872&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;9197421a-f0b7-4eb3-bd5f-e367f846f6ac&quot;&gt;API호출에서 오류 발생하면 오류 레이아웃의 가시성이 true 인지 확인&lt;/p&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;49bac1c8-0ced-4bec-b55a-0f13462db5c2&quot;&gt;&lt;code&gt;@HiltViewModel
class ContactsViewModel @Inject constructor(
    private val contactsRepository: ContactsRepository
) {
    private val _uiState = MutableStateFlow(UIState())
    val uiState = _uiState.asStateFlow()

    init {
        // test that loading indicator visibility state is true when ViewModel first load
         _uiState.update { it.copy(isLoading = true) }
        fetchContacts()
    }

    private fun fetchContacts() = viewModelScope.launch {
        runCatching { 
            // 저장소에서 데이터 가져오기
             contactRepository.getContacts() 
        }.onSuccess { 
            // 로딩 표시기 가시성 테스트 연락처 가져오기 성공 시 상태는 false입니다 
            . // 연락처 가져오기 성공 시 연락처 데이터가 업데이트되어야 하는지 테스트합니다.
             _uiState.update { it.copy(isLoading = false , Contacts = currentContacts) } 
        }.onFailure { 
            // 로딩 표시기 가시성 상태가 다음과 같은지 테스트합니다. 연락처 가져오기에서 오류가 반환되면 false // 연락처 가져 오기 
            에서 오류가 발생하면 오류 레이아웃 가시성이 true인지 테스트합니다.
             _uiState.update { it.copy(isLoading = false , isError = true ) } } 
        }
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;24dff535-e4bc-4d35-a04c-60fba911b648&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;ab6643a1-13c9-4460-8fb7-41ebd925cdd7&quot;&gt;테스트&lt;/p&gt;&lt;pre class=&quot;code Kotlin&quot; id=&quot;d6f770ed-ede1-4fd6-a9d4-36e445f7cfa2&quot;&gt;&lt;code&gt;@Test
 fun ` 연락처 가져오기 에서 오류가 발생 하면 오류 레이아웃 가시성이 true 인지  테스트` ()  {}    // Arrange
    whenever(contactsRepository.getContacts()).thenThrow(RuntimeException())
    // Act
    initTestClass()
    
    // Assert
    Truth.assertThat(state.isError).isTrue()
}&lt;/code&gt;&lt;/pre&gt;&lt;p class=&quot;&quot; id=&quot;39433d83-1edb-4058-b432-ed39ddbd309f&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;65b0463d-4e11-4a5c-a5b7-8fad706dbc60&quot;&gt;이제 전부 통과해야함. 하나라도 안 되면, 다음 단계로 넘어가서는 안된다!&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;b5a69d92-a2d5-4966-a052-ce645ba28d39&quot;&gt;
&lt;/p&gt;&lt;p class=&quot;&quot; id=&quot;1847e793-c81e-429c-9845-6795ff317178&quot;&gt;
&lt;/p&gt;&lt;h3 class=&quot;&quot; id=&quot;b989063a-9306-4e9e-8f46-7260c402e886&quot;&gt;5단계 - 리팩터링&lt;/h3&gt;&lt;p class=&quot;&quot; id=&quot;ad429524-923f-4ed4-9884-206b74e62815&quot;&gt;이제 Refactoring 하면 된다~&lt;/p&gt;&lt;html&gt;&lt;body&gt;&lt;br/&gt;&lt;p class=&quot;&quot;&gt;Uploaded by &lt;mark class=&quot;highlight-orange&quot;&gt;&lt;a href=&quot;https://github.com/jmjeon94/N2T&quot;&gt;N2T&lt;/a&gt;&lt;/mark&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;&lt;/div&gt;&lt;/article&gt;&lt;span class=&quot;sans&quot; style=&quot;font-size:14px;padding-top:2em&quot;&gt;&lt;/span&gt;&lt;/body&gt;</description>
      <category>개발/Android</category>
      <category>android</category>
      <category>TDD</category>
      <category>안드로이드</category>
      <author>이도일</author>
      <guid isPermaLink="true">https://dev-soyeon.tistory.com/96</guid>
      <comments>https://dev-soyeon.tistory.com/96#entry96comment</comments>
      <pubDate>Thu, 30 Nov 2023 11:01:11 +0900</pubDate>
    </item>
  </channel>
</rss>