<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>kk_jang93.log</title>
        <link>https://velog.io/</link>
        <description>앱개발을 사랑하는 개발자</description>
        <lastBuildDate>Sun, 24 Nov 2024 13:01:28 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>kk_jang93.log</title>
            <url>https://velog.velcdn.com/images/kk_jang93/profile/6c955be5-8f4f-4b07-8bf6-15ee1833537e/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. kk_jang93.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/kk_jang93" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[대학원 합격 후기]]></title>
            <link>https://velog.io/@kk_jang93/%EB%8C%80%ED%95%99%EC%9B%90-%ED%95%A9%EA%B2%A9-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@kk_jang93/%EB%8C%80%ED%95%99%EC%9B%90-%ED%95%A9%EA%B2%A9-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Sun, 24 Nov 2024 13:01:28 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>일반대학원을 준비하면서 누군가에게 도움이 되고자 공유의 목적으로 작성하였습니다. 
대학원을 준비하시는 분들 모두 힘내시고 화이팅입니다 !</p>
</blockquote>
<p>원하는 대학원에 합격 후 후기를 써야겠다 생각하다가
이제서야 끄적끄적 써봅니다. ㅎㅎ...</p>
<hr>
<h2 id="1-대학원-진학-계기">1. 대학원 진학 계기</h2>
<p>회사 사정이 어려워져 다니던 회사를 갑작스럽게 나오게 되었습니다.
임금 정산이 늦어지고 생각은 많아지고 다시 일어날 힘을 점점 잃게되었는데
마냥 손놓고 있을순 없기에 예전부터 꿈꾸었던 <em><strong>대학원</strong></em> 을 생각하게되었습니다. 갑작스럽게 대학원을 어떻게 준비해야하는가 부터 모든 과정이 우여곡절이 많을 것이라 생각하였지만 현재의 상황이 위기가 아닌 기회로 받아들이자는 마음가짐으로 새로 마인드셋을 하면서
<em><strong>&quot;아무렴 뭐 어때, 이참에 대학원을 도전해보자 !&quot;</strong></em> 라는 마음을 먹게 되었습니다.</p>
<p>로보어드바이저와 조각투자 등 금융 카테고리를 가진 스타트업에서 개발자로 일을 해보니 개발 지식 뿐 아니라 금융에 대하여 전반적인 개념과 이론을 머릿속에 집어넣어야 겠다고 생각했습니다. 그리고 직장과 병행하는 특수대학원도 고려를 안해본 것은 아니지만 더욱 더 깊이있는 학문과 연구를 하고싶은 마음에 <strong><em>일반대학원</em></strong> 진학을 하기로 결심하였습니다.</p>
<p>여러 대학원을 알아가는 과정속에서 정말 가고싶다고 생각이 되었던 학교과 학과는 </p>
<ul>
<li>성균관대학교 대학원 - 핀테크 융합전공</li>
</ul>
<p>이였습니다. 해당 학과의 연구와 커리큘럼을 보고 처음으로 무엇인가 배워보고 연구해보고싶다는 생각이 들었습니다.</p>
<h2 id="2-준비-과정">2. 준비 과정</h2>
<p><a href="https://gradschool.skku.edu/grad/mojip/regul.htm">성균관대학교 모집 요강 공식 사이트</a></p>
<p>우선, 대학원 홈페이지에 들어가서 입시 모집 요강을 보았습니다.
원서 접수는 모두 <strong>진학사 어플라이</strong> 에서 진행하였습니다.</p>
<p><img src="https://velog.velcdn.com/images/kk_jang93/post/ab0949ae-2415-4b5b-ad65-b34981171312/image.png" alt=""></p>
<p>원서 접수에서 입학원서와 학업계획서를 작성하여 제출하는데
처음 작성해본 학업계획서여서 막막하였습니다.
작성해야할 목록은 아래와 같습니다.</p>
<ul>
<li>자신의 학문적 지향 ( 2800 Byte )</li>
<li>진학동기 및 목표 ( 2800 Byte )</li>
<li>미래의 연구계획 ( 2800 Byte )</li>
<li>기타 ( 2800 Byte )</li>
</ul>
<p>저같은 경우에는 개발자로써 어려움을 해결한 경험과 계획,연구,성과 순으로 경험을 작성하였습니다. 지나고나서 돌이켜보니 학업계획서에는 정답이 없고 개인의 주관과 지원 동기가 확실히만 어필된다면 완벽 한 것 같습니다. <del>저는 망했어요..ㅎㅎ..</del></p>
<p>모든 작성을 완료 후 원서 접수 마감일 하루전 무사히 마쳤습니다.
다음날, 성균관 대학교에 방문하여 서류 제출도 무사히 마쳤고 간김에 학교 구경도 한번 해보았습니다. </p>
<blockquote>
<p>이외의 추가 서류</p>
</blockquote>
<ul>
<li>학사 졸업증명서</li>
<li>학사 성적증명서</li>
<li>공인어학시험 (최근 2년내)
학과마다 추가의 서류가 있으므로 모집 요강 확인 필수!!</li>
<li><blockquote>
<p><a href="https://velog.velcdn.com/images/kk_jang93/post/0d9b4ed7-560f-4049-ad75-dfad4f4d1311/image.pdf">성균관대학교 대학원 2025 전기 모집 요강</a></p>
</blockquote>
</li>
</ul>
<h2 id="3-대학원-면접">3. 대학원 면접</h2>
<p>일부 학과를 제외한 나머지 학과들은 전부 대면 면접으로 진행되었습니다.
이전에 서류 제출을 하기 위하여 학교를 방문하였을때 이곳저곳 돌아다니면서 동선을 미리 파악해두었던 것이 도움이 되었습니다. </p>
<p>저의 경우 면접 일정은 아래와 같았어요. </p>
<blockquote>
<h4 id="10월-26일-토-am-10시"><strong>10월 26일 (토) AM 10시</strong></h4>
<p>면접지원자의 경우 모두 <strong><em>AM 9시 30분</em></strong> 까지 면접 대기실에 입장하여 대기요망</p>
</blockquote>
<p>아슬아슬하게 도착하는것 보단 커피를 사서 여유롭게 가는게 좋은 것 같습니다.
화장실도 가고 학교도 둘러볼겸 9시 도착으로 계획하고 진행 하였습니다.
물론, 복장은 정장을 입고 갔습니다. 대학원 면접에 무슨 정장이냐 하시는 분들도 계신데 저는 아무래도 깔끔하고 단정하게 보이고 싶어서 정장을 입었습니다.
대기실에 도착하니 저보다 빨리 오신 분들이 5명 정도 있었습니다.
추후, 10시정도가 되었을땐 어림잡아 30~40명 정도 계셨습니다.
저의 경우 순번이 마지막 조에 편성되어서 하염없이 대기실에서 기다려야했습니다 ㅠ..
면접 대기실에서 대략 1시간 정도 대기하였으며, 대기 시간 동안 준비해온 자기소개와 지원 동기 , 학업 계획에 대하여 암기를 진행하였습니다.
프린트물로 준비해오셔서 외우시는 분도 계셨고 휴대폰에 적어 외우시는분들도 적지 않게 보았습니다.</p>
<h2 id="4-면접-후">4. 면접 후</h2>
<p>면접은 면접관 2 / 지원자 3 의 형태로 진행되었습니다.
1명씩 돌아가면서 자기소개와 지원동기를 포함하여 30초 ~ 1분 내외로 자기소개를 하였고 각각 개별 질문은 2~3가지의 내용으로 진행되었습니다.
긴장을 많이 풀어주시려고 하셨으며 편안한 분위기에서 진행되었습니다.
면접 대기실에 있을때 까지만 해도 긴장이 전혀 안되고 떨림이란게 안느껴졌었는데 면접 대기실을 떠나 면접 장소 문을 여는 순간 심장이 터지는줄 알았습니다.
긴장을 엄청 하였지만 그래도 준비한 만큼, 간절한 만큼 후회없이 보고 돌아오자 마음을 먹고 자신감과 패기로 답변을 하였습니다.
마지막에 할말이 있냐는 질문에는 사실 아무말도 못하고 나오긴 하였지만요..
그래도 속은 후련했습니다.</p>
<h2 id="5-대학원-결과">5. 대학원 결과</h2>
<blockquote>
<p>합격자 발표일 : 11월 7일(목) 17:00</p>
</blockquote>
<p>면접 이후 약 2주 뒤에 결과 발표를 하였습니다.
운동도 하고 공부도 하며 생각보다 시간이 빠르게 흘러가는 것을 느끼며
노트북과 휴대폰을 챙겨 카페로 갔습니다.
사실 집에서 결과 확인을 해도 괜찮았는데 커피가 너무 마시고 싶어서 카페로 갔어요.. ㅎㅎ..
결과 발표 당일날 발표시간이 <em><strong>17:00</strong></em> 라는게 조금 신경쓰이고 &quot;<del>시간이 정말 안간다.</del>&quot; 라고 느꼈지만 긴장감을 가지며 하염없이 기다렸습니다.
시간이 흘러 17:00가 되었고 휴대폰으로 문자가 왔습니다.
<del>&gt; 결과 발표가 나왔으니 홈페이지에서 확인해보세요</del></p>
<p>합격자 조회 홈페이지에서 이름과 수험번호를 입력하여 확인해보았습니다.</p>
<h3 id="결과는">결과는....</h3>
<p><img src="https://velog.velcdn.com/images/kk_jang93/post/64cc8926-b0e2-4e52-a6a9-c026a150f621/image.png" alt=""></p>
<p>...합격....?!?!?!?!?</p>
<p>사실 <strong>합격</strong> 글자와 저 화면을 보았을때 카페에서 소리질렀습니다
&quot; ㅠㅠㅠ노력한 보람이 있구나! &quot;
갑작스럽게 회사를 나오게 되어 부랴부랴 대학원 진학에 대하여 막연히 생각만 했었고 현실적으로 여러가지 고민과 더불어 <em>할 수 있을까?</em> 란 의구심이 항상 들어 마음이 불안안 상태로 꽤 오랜 시간 지내왔는데 한방에 다 녹아서 없어지는 느낌이 들었습니다. 잠도 안자며 노력한 결과에 대한 보상을 받은 기분이라 성취감이 너무 대단했습니다. 일단 하면 된다..! 를 너무 깨닫게 되었던 시간이였습니다.
혹시나 대학원을 준비하시는 분들이나 고민중이신 분들이라면
주저하지 말고 도전해보시는걸 권해드리고 싶습니다.
왜냐하면 저같은 쫌쫌따리 개발자도 되었으니까요 ! </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] Compose Effect 비교]]></title>
            <link>https://velog.io/@kk_jang93/Android-Compose-Effect-%EB%B9%84%EA%B5%90</link>
            <guid>https://velog.io/@kk_jang93/Android-Compose-Effect-%EB%B9%84%EA%B5%90</guid>
            <pubDate>Sun, 13 Oct 2024 07:02:00 GMT</pubDate>
            <description><![CDATA[<h2 id="disposableeffect">DisposableEffect</h2>
<blockquote>
<p>사용 케이스</p>
</blockquote>
<ul>
<li>이벤트 리스너를 등록하고 제거할 때</li>
<li>애니메이션을 시작하고 정지 할 때</li>
<li>카메라와 LocationManager와 같이 센서 리소스를 바인딩 또는 언바인딩 할 때</li>
<li>DB 연결을 관리 할 때</li>
</ul>
<h2 id="launchedeffect">LaunchedEffect</h2>
<blockquote>
<p>사용 케이스</p>
</blockquote>
<ul>
<li>네트워크로부터 데이터를 가져 올 때</li>
<li>이미지 프로세싱을 수행 할 때</li>
<li>DB를 업데이트 할 때</li>
</ul>
<h2 id="sideeffect">SideEffect</h2>
<blockquote>
<p>사용 케이스</p>
</blockquote>
<ul>
<li>로깅 및 분석을 위한 코드를 사용 할 때</li>
<li>블루투스 장치에 연결을 하기 위하여 한번 초기화를 진행 할 때</li>
<li>파일로부터 데이터를 최초 한번 로딩 할 때</li>
<li>라이브러리를 초기화 할 때</li>
</ul>
<h3 id="결론">결론</h3>
<blockquote>
<p>SideEffect, DisposableEffect , LaunchedEffect 의 주요 차이점 요약</p>
</blockquote>
<ul>
<li>SideEffect 은 상위 컴포저블이 재구성 될 때 실행되며 컴포저블의 상태나 속성에 의존하지 않는 작업을 실행하는데 유용</li>
<li>DisposableEffect 는 상위 컴포저블이 처음 렌더링 될 때 실행되며 컴포저블이 더 이상 사용되지 않을 때 정리가 필요한 리소스를 관리하는 데 유용합니다. 첫 번째 컴포지션 또는 키가 변경 될 때 트리거 되며 종료 시 onDispose() 메소드를 호출합니다.</li>
<li>LaunchedEffect 는 별도의 코루틴 스코프에서 부수효과를 실행하며 UI 쓰레드를 차단하지 않고 시간이 오래 걸리는 작업을 실행하는 데 유용합니다. 첫 번째 컴포지션이나 키가 변경 시 트리거 됩니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] 생명주기]]></title>
            <link>https://velog.io/@kk_jang93/Flutter-%EC%83%9D%EB%AA%85%EC%A3%BC%EA%B8%B0</link>
            <guid>https://velog.io/@kk_jang93/Flutter-%EC%83%9D%EB%AA%85%EC%A3%BC%EA%B8%B0</guid>
            <pubDate>Thu, 12 Sep 2024 07:53:05 GMT</pubDate>
            <description><![CDATA[<ol>
<li><p>createState() : Framework가 StatefulWidget을 만들경우 createState() 가 즉시 호출된다.</p>
</li>
<li><p>initState() : widget이 만들어지고 생성자 후에 처음 메소드 실행할때 이 함수가 실행된다. (super.initState() 필수)</p>
</li>
<li><p>didChangeDependencies() : 이 함수는 initState를 호출한 뒤에 실행된다. 또한 이 위젯은 데이터에 의존하는 객체가 호출될 때마다 호출됩니다. InheritedWidget 에 의존하는 경우 업데이틀 합니다.</p>
</li>
<li><p>build() : 필수적으로 오버라이딩해서 구현해야되는 함수이다. 위젯을 리턴한다.</p>
</li>
<li><p>didUpdateWidget() : 만약 부모 위젯이 업데이트가 되거나 이 위젯이 다시 만들 경우 이 함수가 호출되고 같은 runtimeType (이건 또 뭐징?) 을 함께 다시 만들어진다.</p>
</li>
<li><p>setState() : 데이터가 변경된 후 호출됨</p>
</li>
<li><p>deactivate() : State가 제거될때 호출됨 (거의 사용안함)</p>
</li>
<li><p>dispose() : 영구적인 State Object가 삭제될때 호출된다. 이 함수는 주로 Stream 이나 애니메이션 을 해제시 사용된다.</p>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] Flutter 개발 환경 시작하기]]></title>
            <link>https://velog.io/@kk_jang93/Flutter-Flutter-%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@kk_jang93/Flutter-Flutter-%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 05 Aug 2024 01:30:17 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Flutter 개발 환경 셋팅 및 초기 가이드</p>
</blockquote>
<h2 id="1-flutter-sdk-다운로드">1. Flutter SDK 다운로드</h2>
<p><a href="https://docs.flutter.dev/release/archive?tab=windows">https://docs.flutter.dev/release/archive?tab=windows</a></p>
<p>노트북 스펙 : <strong>MacBook Pro Apple M1</strong></p>
<h4 id="240805-기준-최신-버전-arm-64-다운로드를-진행">24.08.05 기준 최신 버전 arm 64 다운로드를 진행</h4>
<p><img src="https://velog.velcdn.com/images/kk_jang93/post/68182fce-73b1-4186-a780-05072bb56cd6/image.png" alt=""></p>
<h2 id="2-환경-변수-설정">2. 환경 변수 설정</h2>
<pre><code>$ open ~/.zshrc 또는 
$ open ~/.bash_profile

#Flutter
export PATH=&quot;$PATH:/Users/${본인 Flutter SDK 경로}&quot;

-&gt; 예시: export PATH=&quot;$PATH:/Users/kkjang/flutter/bin&quot;

환경변수 입력 완료 후 저장하고 닫기

$ source ~/.zshrc 또는
$ source ~/.bash_profile
</code></pre><h2 id="3-flutter-doctor">3. flutter doctor</h2>
<pre><code>&gt; $ flutter doctor 실행</code></pre><p>**[v] 이미 Flutter 를 실행하기에 조건 충분합니다. **</p>
<p><strong>[x] Android Studio 에러</strong>
Android Studio 설치안한 것이므로 Android Studio 설치 !</p>
<p><strong>[x] Android license status unknown 에러</strong>
그 밑에 나오는 flutter doctor --android-licenses 이걸 터미널에 입력하고 모두 동의하면 끝 !</p>
<p><strong>[x] cmdline-tools component is missing 에러</strong>
위에서 Android Studio 에서 SDK Manager 설치 과정중 일부 설치가 누락된 것입니다.</p>
<p>**[x] Chrome 에러 **
크롬 브라우저가 없으므로 크롬을 설치해주세요. </p>
<p><strong>[x] xcode, cocoapods 에러</strong>
iOS 개발에 필요한 것이며, xcode 및 cocoapds 설치를 진행해야합니다.</p>
<p><img src="https://velog.velcdn.com/images/kk_jang93/post/0a27e012-0826-4029-9503-f562d8a16bd3/image.png" alt=""></p>
<h2 id="4-android-studio-셋팅">4. Android Studio 셋팅</h2>
<blockquote>
<p>다른 IDE를 사용하고 싶어도 일단 Android Studio 셋팅은 해놓고 다른 에디터 사용해야 합니다.</p>
</blockquote>
<ol>
<li>flutter 검색</li>
<li>flutter Install</li>
</ol>
<p><img src="https://velog.velcdn.com/images/kk_jang93/post/4c4e1477-abdf-4ff3-a14b-3b7ecee41f29/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kk_jang93/post/47c8fa00-d115-47bc-b515-b467bdd9798d/image.png" alt=""></p>
<ol start="3">
<li>SDK Tools</li>
<li>Android SDK Command-line Tools (lastest) 체크 </li>
<li>Apply -&gt; Ok</li>
</ol>
<h2 id="5-xcode--cocoapods-install">5. Xcode , Cocoapods Install</h2>
<ol>
<li>AppStore - <strong>Xcode Install</strong></li>
<li>아래 명령어 순서대로 입력</li>
</ol>
<pre><code>$ sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
$ sudo xcodebuild -runFirstLaunch</code></pre><ol start="3">
<li>완료 후 Xocde License 에 서명을 진행</li>
</ol>
<pre><code>$ sudo xcodebuild -license</code></pre><ol start="4">
<li>Space 를 눌러 단락 이동을 한다. ( <strong>최하단</strong>으로 이동 )</li>
</ol>
<blockquote>
<p>By typing &#39;agree&#39; you are agreeing to the terms of the software license agreements. Type &#39;print&#39; to print them or anything else to cancel, [agree, print, cancel]</p>
</blockquote>
<ol start="5">
<li>위와 같은 메시지 출력시 <strong>agree</strong> 를 입력 후 <strong>Enter</strong> !</li>
<li><strong>cocoapods Install</strong></li>
</ol>
<pre><code>$ sudo gem install cocoapods</code></pre><h2 id="6-flutter-app-init">6. Flutter App Init</h2>
<p><img src="https://velog.velcdn.com/images/kk_jang93/post/42b469d1-e5f1-4516-b42b-e9f65ce6139d/image.png" alt=""><img src="https://velog.velcdn.com/images/kk_jang93/post/4f5a1648-df2c-4447-b4b5-3cb3fc298002/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kk_jang93/post/9e424c4b-8405-4e4f-8f6a-1d05b79aeb95/image.png" alt=""></p>
<h3 id="flutter-tutorial-app-만들기-종료-"><em>Flutter Tutorial App</em> 만들기 종료 !</h3>
<br>
<br><br>

<p>참고 &amp; Docs
<a href="https://docs.flutter.dev/release/archive?tab=windows">https://docs.flutter.dev/release/archive?tab=windows</a>
<a href="https://codingapple.com/unit/flutter-install-on-windows-and-mac/">https://codingapple.com/unit/flutter-install-on-windows-and-mac/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Git] 커밋 메시지 컨벤션 Commit Message Convention]]></title>
            <link>https://velog.io/@kk_jang93/Git-%EC%BB%A4%EB%B0%8B-%EB%A9%94%EC%8B%9C%EC%A7%80-%EC%BB%A8%EB%B2%A4%EC%85%98-Commit-Message-Convention</link>
            <guid>https://velog.io/@kk_jang93/Git-%EC%BB%A4%EB%B0%8B-%EB%A9%94%EC%8B%9C%EC%A7%80-%EC%BB%A8%EB%B2%A4%EC%85%98-Commit-Message-Convention</guid>
            <pubDate>Thu, 01 Aug 2024 04:16:54 GMT</pubDate>
            <description><![CDATA[<p>알아보기 쉽게 정리한 <strong>Git Commit Message Convention</strong></p>
<h2 id="1-commit-message-structure">1. Commit Message Structure</h2>
<ul>
<li>기본적인 커밋 메시지 구조 (각 파트는 빈줄로 구분한다.)</li>
</ul>
<blockquote>
<p>제목 (Type: Subject)
(한줄 띄어 분리)
본문 (Body)
(한줄 띄어 분리)</p>
</blockquote>
<h2 id="2-commit-type">2. Commit Type</h2>
<ul>
<li>커밋의 타입 구성</li>
</ul>
<blockquote>
<p>태그: 제목
:(space)제목 으로 :뒤에만 space를 넣는다.</p>
</blockquote>
<table>
<thead>
<tr>
<th align="left">Tag Name</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td align="left">Feat</td>
<td>새로운 기능을 추가</td>
</tr>
<tr>
<td align="left">Fix</td>
<td>버그 수정</td>
</tr>
<tr>
<td align="left">Design</td>
<td>CSS 등 사용자 UI 디자인 변경</td>
</tr>
<tr>
<td align="left">!BREAKING CHANGE</td>
<td>커다란 API 변경의 경우</td>
</tr>
<tr>
<td align="left">!HOTFIX</td>
<td>급하게 치명적인 버그를 고쳐야하는 경우</td>
</tr>
<tr>
<td align="left">Style</td>
<td>코드 포맷 변경, 세미 콜론 누락, 코드 수정이 없는 경우</td>
</tr>
<tr>
<td align="left">Refactor</td>
<td>프로덕션 코드 리팩토링</td>
</tr>
<tr>
<td align="left">Comment</td>
<td>필요한 주석 추가 및 변경</td>
</tr>
<tr>
<td align="left">Docs</td>
<td>문서 수정</td>
</tr>
<tr>
<td align="left">Test</td>
<td>테스트 코드, 리펙토링 테스트 코드 추가, Production Code(실제로 사용하는 코드) 변경 없음</td>
</tr>
<tr>
<td align="left">Chore</td>
<td>빌드 업무 수정, 패키지 매니저 수정, 패키지 관리자 구성 등 업데이트, Production Code 변경 없음</td>
</tr>
<tr>
<td align="left">Rename</td>
<td>파일 혹은 폴더명을 수정하거나 옮기는 작업만인 경우</td>
</tr>
<tr>
<td align="left">Remove</td>
<td>파일을 삭제하는 작업만 수행한 경우</td>
</tr>
</tbody></table>
<p>추가적인 문맥 정보를 제공하기 위한 목적으로 괄호 안에 적을 수도 있다.</p>
<pre><code>ex)
 Feat(navigation)
 Fix(db)</code></pre><h2 id="3-subject">3. Subject</h2>
<ul>
<li>제목은 50글자 이내로 작성한다.</li>
<li>첫글자는 대문자로 작성한다.</li>
<li>마침표 및 특수기호는 사용하지 않는다.</li>
<li>영문으로 작성하는 경우 동사(원형)을 가장 앞에 명령어로 작성한다.</li>
<li>과거시제는 사용하지 않는다.</li>
<li>간결하고 요점적으로 즉, 개조식 구문으로 작성한다.</li>
</ul>
<pre><code>ex)
Fixed --&gt; Fix
Added --&gt; Add
Modified --&gt; Modify</code></pre><h2 id="4-body">4. Body</h2>
<ul>
<li>72이내로 작성한다.</li>
<li>최대한 상세히 작성한다. (코드 변경의 이유를 명확히 작성할수록 좋다)</li>
<li>어떻게 변경했는지보다 무엇을, 왜 변경했는지 작성한다.</li>
</ul>
<h2 id="5-footer">5. Footer</h2>
<ul>
<li><p>선택사항</p>
</li>
<li><p>issue tracker ID 명시하고 싶은 경우에 작성한다.</p>
</li>
<li><p>유형: #이슈 번호 형식으로 작성한다.</p>
</li>
<li><p>여러 개의 이슈번호는 쉼표(,)로 구분한다.</p>
</li>
<li><p>이슈 트래커 유형은 다음 중 하나를 사용한다.
Fixes: 이슈 수정중 (아직 해결되지 않은 경우)
Resolves: 이슈를 해결했을 때 사용
Ref: 참고할 이슈가 있을 때 사용
Related to: 해당 커밋에 관련된 이슈번호 (아직 해결되지 않은 경우)</p>
</li>
</ul>
<pre><code>ex) 
Fixes: #45 Related to: #34, #23</code></pre><h2 id="6-example">6. Example</h2>
<h4 id="예시-1">예시 1</h4>
<pre><code>Feat: 회원 가입 기능 구현

SMS, 이메일 중복확인 API 개발

Resolves: #123
Ref: #456
Related to: #48, #45</code></pre><h4 id="예시-2">예시 2</h4>
<pre><code>feat: Summarize changes in around 50 characters or less

More detailed explanatory text, if necessary. Wrap it to about 72
characters or so. In some contexts, the first line is treated as the
subject of the commit and the rest of the text as the body. The
blank line separating the summary from the body is critical (unless
you omit the body entirely); various tools like `log`, `shortlog`
and `rebase` can get confused if you run the two together.

Explain the problem that this commit is solving. Focus on why you
are making this change as opposed to how (the code explains that).
Are there side effects or other unintuitive consequences of this
change? Here&#39;s the place to explain them.

Further paragraphs come after blank lines.

- Bullet points are okay, too

- Typically a hyphen or asterisk is used for the bullet, preceded
by a single space, with blank lines in between, but conventions
vary here

If you use an issue tracker, put references to them at the bottom,
like this:

Resolves: #123
See also: #456, #789</code></pre><h4 id="그-외-자주-쓰이는">그 외 자주 쓰이는</h4>
<pre><code>Fix : 버그 수정
  Fix my test
  Fix typo in style.css
  Fix my test to return undefined
  Fix error when using my function

  Update : Fix와 달리 원래 정상적으로 동작했지만 보완의 개념
  Update harry-server.js to use HTTPS

  Add
  Add documentation for the defaultPort option
  Add example for setting Vary: Accept-Encoding header in zlib.md

  Remove(Clean이나 Eliminate) : ‘unnecessary’, ‘useless’, ‘unneeded’, ‘unused’, ‘duplicated’가 붙는 경우가 많음
  Remove fallback cache
  Remove unnecessary italics from child_process.md

  Refactor : 리팩토링

  Simplify : Refactor와 유사하지만 약한 수정, 코드 단순화

  Improve : 호환성, 테스트 커버리지, 성능, 검증 기능, 접근성 등의 향상
  Improve iOS&#39;s accessibilityLabel performance by up to 20%

  Implement : 코드 추가보다 큰 단위의 구현
  Implement bundle sync status

  Correct : 주로 문법의 오류나 타입의 변경, 이름 변경 등에 사용
  Correct grammatical error in BUILDING.md

  Prevent
  Prevent hello handler from saying Hi in hi.js

  Avoid : Prevent는 못하게 막지만, Avoid는 회피(if 등)
  Avoid flusing uninitialized traces

  Move : 코드나 파일의 이동
  Move function from header to source file

  Rename : 이름 변경
  Rename node-report to report</code></pre><h2 id="7-commit-message-emoji-gitmoji">7. Commit Message Emoji (gitmoji)</h2>
<table>
<thead>
<tr>
<th align="left">Tag Name</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td align="left">Emoji</td>
<td>Description</td>
</tr>
<tr>
<td align="left">🎨</td>
<td>코드의 형식 / 구조를 개선 할 때</td>
</tr>
<tr>
<td align="left">📰</td>
<td>새 파일을 만들 때</td>
</tr>
<tr>
<td align="left">📝</td>
<td>사소한 코드 또는 언어를 변경할 때</td>
</tr>
<tr>
<td align="left">🐎</td>
<td>성능을 향상시킬 때</td>
</tr>
<tr>
<td align="left">📚</td>
<td>문서를 쓸 때</td>
</tr>
<tr>
<td align="left">🐛</td>
<td>버그 reporting할 때, @FIXME 주석 태그 삽입</td>
</tr>
<tr>
<td align="left">🚑</td>
<td>버그를 고칠 때</td>
</tr>
<tr>
<td align="left">🐧</td>
<td>리눅스에서 무언가를 고칠 때</td>
</tr>
<tr>
<td align="left">🍎</td>
<td>Mac OS에서 무언가를 고칠 때</td>
</tr>
<tr>
<td align="left">🏁</td>
<td>Windows에서 무언가를 고칠 때</td>
</tr>
<tr>
<td align="left">🔥</td>
<td>코드 또는 파일 제거할 때 , @CHANGED주석 태그와 함께</td>
</tr>
<tr>
<td align="left">🚜</td>
<td>파일 구조를 변경할 때 . 🎨과 함께 사용</td>
</tr>
<tr>
<td align="left">🔨</td>
<td>코드를 리팩토링 할 때</td>
</tr>
<tr>
<td align="left">☔️</td>
<td>테스트를 추가 할 때</td>
</tr>
<tr>
<td align="left">🔬</td>
<td>코드 범위를 추가 할 때</td>
</tr>
<tr>
<td align="left">💚</td>
<td>CI 빌드를 고칠 때</td>
</tr>
<tr>
<td align="left">🔒</td>
<td>보안을 다룰 때</td>
</tr>
<tr>
<td align="left">⬆️</td>
<td>종속성을 업그레이드 할 때</td>
</tr>
<tr>
<td align="left">⬇️</td>
<td>종속성 다운 그레이드 할 때</td>
</tr>
<tr>
<td align="left">⏩</td>
<td>이전 버전 / 지점에서 기능을 전달할 때</td>
</tr>
<tr>
<td align="left">⏪</td>
<td>최신 버전 / 지점에서 기능을 백 포트 할 때</td>
</tr>
<tr>
<td align="left">👕</td>
<td>linter / strict / deprecation 경고를 제거 할 때</td>
</tr>
<tr>
<td align="left">💄</td>
<td>UI / style 개선시</td>
</tr>
<tr>
<td align="left">♿️</td>
<td>접근성을 향상시킬 때</td>
</tr>
<tr>
<td align="left">🚧</td>
<td>WIP (진행중인 작업)에 커밋, @REVIEW주석 태그와 함께 사용</td>
</tr>
<tr>
<td align="left">💎</td>
<td>New Release</td>
</tr>
<tr>
<td align="left">🔖</td>
<td>버전 태그</td>
</tr>
<tr>
<td align="left">🎉</td>
<td>Initial Commit</td>
</tr>
<tr>
<td align="left">🔈</td>
<td>로깅을 추가 할 때</td>
</tr>
<tr>
<td align="left">🔇</td>
<td>로깅을 줄일 때</td>
</tr>
<tr>
<td align="left">✨</td>
<td>새로운 기능을 소개 할 때</td>
</tr>
<tr>
<td align="left">⚡️</td>
<td>도입 할 때 이전 버전과 호환되지 않는 특징, @CHANGED주석 태그 사용</td>
</tr>
<tr>
<td align="left">💡</td>
<td>새로운 아이디어, @IDEA주석 태그</td>
</tr>
<tr>
<td align="left">🚀</td>
<td>배포 / 개발 작업 과 관련된 모든 것</td>
</tr>
<tr>
<td align="left">🐘</td>
<td>PostgreSQL 데이터베이스 별 (마이그레이션, 스크립트, 확장 등)</td>
</tr>
<tr>
<td align="left">🐬</td>
<td>MySQL 데이터베이스 특정 (마이그레이션, 스크립트, 확장 등)</td>
</tr>
<tr>
<td align="left">🍃</td>
<td>MongoDB 데이터베이스 특정 (마이그레이션, 스크립트, 확장 등)</td>
</tr>
<tr>
<td align="left">🏦</td>
<td>일반 데이터베이스 별 (마이그레이션, 스크립트, 확장명 등)</td>
</tr>
<tr>
<td align="left">🐳</td>
<td>도커 구성</td>
</tr>
<tr>
<td align="left">🔇</td>
<td>로깅을 줄일 때</td>
</tr>
<tr>
<td align="left">🤝</td>
<td>파일을 병합 할 때</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] Hilt @InstallIn]]></title>
            <link>https://velog.io/@kk_jang93/Android-Hilt-InstallIn</link>
            <guid>https://velog.io/@kk_jang93/Android-Hilt-InstallIn</guid>
            <pubDate>Wed, 31 Jul 2024 04:44:57 GMT</pubDate>
            <description><![CDATA[<p>Hilt를 사용할 경우 모듈 클래스에 반드시 @InstallIn을 추가해야 한다. 그렇지 않으면 컴파일 타임에 오류가 발생한다.</p>
<p>하지만 Dagger2에서 Hilt로 마이그레이션을 하거나 특별한 사유가 있는 경우 모든 모듈 클래스에 @InstallIn을 추가하기 어려운 경우가 있다. 이때 다음 예제코드 처럼, 해당 모듈의 그레이들 스크립트에 disableModulesHaveInstallInCheck 옵션을 추가 할 수 있다.</p>
<pre><code>// build.gradle.kts
android {
    ...
    defaultConfig {
    ...                  
    javaCompileOptions.annotationProcessorOptions.arguments[&quot;dagger.hilt.disableModulesHaveInstallInCheck&quot;] = &quot;true&quot;
    }
}</code></pre><p>gradle 명령어를 사용하여 컴파일(빌드)하는 경우에는 다음과 같이 플래그를 추가 할 수 있다.</p>
<pre><code>-Adagger.hilt.disableModulesHaveInstallInCheck=true.</code></pre><h4 id="또는">또는</h4>
<p>개별적으로 모듈 클래스에 @DisableInstallInCheck 를 추가하여 @InstallIn 검사를 비활성화 시킬 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] invoke 란?]]></title>
            <link>https://velog.io/@kk_jang93/Android-invoke-%EB%9E%80</link>
            <guid>https://velog.io/@kk_jang93/Android-invoke-%EB%9E%80</guid>
            <pubDate>Sat, 27 Jul 2024 04:25:43 GMT</pubDate>
            <description><![CDATA[<h2 id="invoke-란">invoke 란?</h2>
<p>코틀린에는 <strong>invoke</strong>라는 특별한 함수, 정확히는 연산자가 존재한다. invoke연산자는 이름 없이 호출될 수 있다. 이름 없이 호출된 다는 의미를 파악하기 위해 아래의 코드를 보자.</p>
<pre><code>object MyFunction {
    operator fun invoke(str: String): String {
        return str.toUpperCase() // 모두 대문자로 바꿔줌
    }
}</code></pre><p>MyFunction이라는 오브젝트 하나가 있다. obeject키워드로 만들었기 때문에 MyFunction은 하나의 객체처럼 사용될 수 있다. 즉, 하나의 객체이기 때문에 객체 안의 메서드를 호출하기 위해서 아래와 같이 호출하고 싶을 것이다.</p>
<p>MyFunction.invoke(&quot;hello&quot;) // HELLO
물론 잘 동작하지만, kotlin에서 invoke라는 이름으로 만들어진 함수는 특별한 힘을 갖는다. 이름 없이 실행될 수 있는 힘이다. 즉, 아래와 같이 호출이 가능하다.</p>
<p>MyFunction(&quot;hello&quot;) // HELLO
MyFunction은 객체다. 그렇기 때문에 MyFunction을 print해보면 MyFunction의 주소값만 출력될 뿐이다. 그런데 MyFunction안에 invoke()함수가 정의 되어있으므로 MyFunction에서 메서드 이름 없이 바로 호출한 것이다. 물론 파라미터를 받을 창구가 있어야 하므로 ()안에 파라미터를 넣어서 실행이 가능하다.</p>
<p>연산자
이렇듯 분명히 invoke와 같이 이름을 부여한 함수임에도 불구하고 실행을 간편하게 할 수 있게 하는 것들을 연산자라고 부른다. 그런 연산자들 몇 개를 코틀린에서 미리 정해놓았다. 대표적으로 + 연산자가 있다. 간단히 예제 하나를 보면서 연산자가 무엇인지 알고 다음으로 넘어가자.</p>
<pre><code>object Sample {
    operator fun plus(str: String):String {
        return this.toString() + str
    }
}</code></pre><pre><code>main() {
    Sample + &quot; Hello~!&quot; // [Sample의 주소값] Hello~!
}</code></pre><p>실행해보면 [Smaple의 주소값]부분에는 실제로 주소값이 들어가고, 그 뒤에 바로 Hello~!라는 글자가 붙을 것이다. 즉 plus라는 이름으로 함수를 만들었지만 plus는 코틀린에서 연산자로 정의해 놓았으므로 plus연산자를 호출하기 위해 틀별히 + 기호를 사용해서 호출한다.</p>
<p><strong>람다는 invoke 함수를 가진 객체다.</strong>
코틀린은 람다를 지원한다. 예를 들어 아래와 같은 람다 함수가 있다고 하자. 이미 코틀린이 기본으로 제공하는 함수를 한 번 감싸는 의미 없는 코드이지만 이해를 위해 작성해보았다.</p>
<p>val toUpperCase = { str: String -&gt; str.toUpperCase() }
모든 값, 또는 값을 담는 변수에는 타입이 있다. Int일 수도, String일 수도, 또는 우리가 만든 클래스 타입일 수도 있다. 그렇다면 위의 toUpperCase는 타입이 무엇일까? Int도, String도 아니다. String을 받고, 다시 String을 반환하는 (String) -&gt; String 타입이다.</p>
<p>그러나 (String) - String은 좀 생소하다. 더 정확히 어떤 걸 의미하는 것일까? 사실 위 타입은 코틀린 표준 라이브러리에 정의된 Function1&lt;P1, R&gt;인터페이스 타입이다. Function&lt;P1, R&gt;의 구현을 살펴보면 invoke(P1) : R 연산자 하나만 달랑 존재한다. 위에서 언급했듯이 invoke연산자는 이름 없이 호출할 수 있다. 이름 없는 함수인 람다와 연관성이 조금 보인다.</p>
<p>결국 위에서 작성한 toUpperCase는 아래 코드와 같다.</p>
<pre><code>val toUpperCase = object : Function1&lt;String, String&gt; {
    override fun invoke(p1: String): String {
        return p1.toUpperCase()
    }
}</code></pre><p>실제로 코틀린에서 작성한 람다는 위의 코드와 같기 때문에 결국 람다도 컴파일 시간에 람다가 할당된 객체로 변환된다는 뜻이다. 개발자는 그것의 invoke를 호출하는 셈이다.</p>
<p>toUpperCase는 현재 invoke라는 연산자 하나를 가진 객체이다. 이제 위에서 언급 알아본대로 toUpperCase.invoke(&quot;hello&quot;)가 아닌 toUpperCase(&quot;hello&quot;)와 같이 호출할 수 있음을 기억한 채로 toUpperCase를 사용해 보자.</p>
<pre><code>fun main() {
    val strList = listOf(&quot;a&quot;,&quot;b&quot;,&quot;c&quot;)
    println(strList.map(toUpperCase)) // [A,B,C]
}</code></pre><p>map함수는 strList의 요소들을 순회하면서 각각의 요소(a,b,c)마다 toUpperCase(요소)를 실행할 것이다. toUpperCase가 invoke연산자를 가지고 있기 때문에 이렇게 편하게 사용 가능한 것이다. 또한 위의 코드는 아래와 같이 작성할 수 있다.</p>
<pre><code>fun main() {
    val strList = listOf(&quot;a&quot;,&quot;b&quot;,&quot;c&quot;)
    println(strList.map {str: String -&gt; str.toUpperCase()}) // [A,B,C]
}</code></pre><p><strong>{str: String -&gt; str.toUpperCase()} 이 부분이 결국 런타임 시 invoke를 하나 가지는 오브젝트로 변환된다.</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] Sealed Class , Enum Class 비교]]></title>
            <link>https://velog.io/@kk_jang93/Android-Sealed-Class-Enum-Class-%EB%B9%84%EA%B5%90</link>
            <guid>https://velog.io/@kk_jang93/Android-Sealed-Class-Enum-Class-%EB%B9%84%EA%B5%90</guid>
            <pubDate>Thu, 25 Jul 2024 03:38:13 GMT</pubDate>
            <description><![CDATA[<p>Kotlin에서는 상황에 따라 _Sealed Class_와 _Enum Class_를 선택할 수 있습니다. 두 가지 모두 각기 다른 장점이 있으며, 사용 목적에 따라 더 적합한 선택이 달라질 수 있습니다. 다음은 각 클래스의 특징과 사용 시점을 비교한 것입니다.</p>
<blockquote>
<h1 id="enum-class">Enum Class</h1>
</blockquote>
<h3 id="특징">특징</h3>
<ul>
<li><strong>상수 집합</strong>: Enum 클래스는 미리 정의된 상수 집합을 나타낼 때 유용합니다.</li>
<li><strong>단순성</strong>: 정의가 단순하고 직관적입니다.</li>
<li><strong>일반적인 사용</strong>: 열거형 상수 간에 비교하거나 특정 상수에 대한 작업을 수행할 때 적합합니다.</li>
<li><strong>상수 메서드</strong>: Enum 상수 각각에 메서드를 정의할 수 있습니다.</li>
</ul>
<h3 id="사용-시점">사용 시점</h3>
<p>상수 값들의 집합이 고정된 경우 (예: 요일, 방향, 상태 등).
간단한 상태나 값을 나타내야 할 때.
각 상수가 동일한 메서드나 속성을 가져야 할 때.</p>
<h3 id="예제">예제</h3>
<pre><code>enum class Direction {
    NORTH, SOUTH, EAST, WEST
}</code></pre><blockquote>
<h1 id="sealed-class">Sealed Class</h1>
</blockquote>
<h3 id="특징-1">특징</h3>
<ul>
<li><strong>계층 구조</strong>: 서브 클래스들이 정의될 수 있으며, 이들 서브 클래스는 모두 같은 파일 내에 정의되어야 합니다.</li>
<li><strong>확장성</strong>: 더 복잡한 데이터 구조나 상태를 표현할 수 있습니다.</li>
<li><strong>패턴 매칭</strong>: when 절에서 사용할 때 모든 가능한 하위 클래스들을 체크하므로 안전성이 높습니다.</li>
<li><strong>불변성</strong>: 불변 데이터를 나타내는 데 유용합니다.</li>
</ul>
<h3 id="사용-시점-1">사용 시점</h3>
<p>데이터의 서브 타입을 나타내고 싶을 때 (예: 다양한 종류의 결과 값, 이벤트 등).
각 상태나 데이터 타입에 대해 다른 속성이나 메서드를 가질 때.
더 복잡한 데이터 모델을 필요로 할 때.</p>
<h3 id="예제-1">예제</h3>
<pre><code>kotlin
sealed class Result {
    data class Success(val data: String) : Result()
    data class Error(val exception: Exception) : Result()
    object Loading : Result()
}</code></pre><hr>
<h3 id="결론">결론</h3>
<p><strong><em>Enum Class</em></strong>는 미리 정의된 상수 값들의 집합을 표현하는 데 적합합니다.
_<strong>Sealed Class</strong>_는 더 복잡한 데이터 구조나 계층을 표현하고, 패턴 매칭을 안전하게 수행하는 데 유리합니다.
따라서, 상황에 따라 두 가지 중 적합한 것을 선택하면 됩니다. 간단한 상수 집합이라면 Enum Class를, 복잡한 데이터 구조와 패턴 매칭이 필요하다면 Sealed Class를 사용하는 것이 좋습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Hilt] Hilt-wokr Issue]]></title>
            <link>https://velog.io/@kk_jang93/Hilt-Hilt-wokr-Issue</link>
            <guid>https://velog.io/@kk_jang93/Hilt-Hilt-wokr-Issue</guid>
            <pubDate>Tue, 23 Jul 2024 07:05:57 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>토이프로젝트를 진행중 빌드 후 문제가 발생하였습니다</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/kk_jang93/post/ad4eaf67-1970-427c-8a03-3c520d4134d5/image.png" alt=""></p>
<p>에러의 내용을 살펴보면 </p>
<p><em>PendingIntent_를 생성할 때 <strong>FLAG_IMMUTABLE</strong> 또는 <strong>FLAG_MUTABLE</strong> 중 하나를 명시적으로 지정해야 합니다. 
일반적으로는 <strong>FLAG_IMMUTABLE을</strong> 사용하는 것이 **_권장</em>** 되며, 
<strong>PendingIntent가</strong> 변경 가능해야 하는 경우
(예: 인라인 답장이나 버블과 같이 사용해야 하는 경우)에만 
<strong>FLAG_MUTABLE을</strong> 사용합니다.</p>
<p>라고 하는데 프로젝트에는 눈씻고 찾아봐도 pendingIntent 를 사용한 곳이 없습니다.</p>
<blockquote>
<p>당연하죠, pendingIntent 를 사용 안했으니까요..</p>
</blockquote>
<p>..? 그럼 뭐때문에 저 에러를 내면서 앱이 죽은거에요..?
<del>ㅇㅁㄴㅇㅁㄴㅇㅁㄴㅁ</del></p>
<hr>
<h3 id="해결과정">해결과정</h3>
<p>자료를 찾아보던 중 androidx.work 라이브러리의 버전이 낮으면 같이 이슈가 발생할 수 도 있다고 나와 있습니다.. ㅎㅎ!
버전을 아무거나 찾아서 넣어 빌드해보니 <del><strong>실패</strong></del>..!</p>
<p>그렇기에 설마 하며 최신 버전의 라이브러리를 implements를 해주니 정상작동하는걸 확인했습니다.
해결 ~!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] 배포 자동화]]></title>
            <link>https://velog.io/@kk_jang93/Android-%EB%B0%B0%ED%8F%AC-%EC%9E%90%EB%8F%99%ED%99%94</link>
            <guid>https://velog.io/@kk_jang93/Android-%EB%B0%B0%ED%8F%AC-%EC%9E%90%EB%8F%99%ED%99%94</guid>
            <pubDate>Wed, 17 Jul 2024 02:16:57 GMT</pubDate>
            <description><![CDATA[<aside>
💡 기존 QA 및 테스트 방식

</aside>

<blockquote>
<p>기존 방식</p>
</blockquote>
<ul>
<li>테스트 진행시 구두 OR 슬랙으로 공지 및 전달사항 공유</li>
<li>테스트 진행<ul>
<li>USB 연결 후 빌드 → 테스트 진행 → 수정 → USB 연결 후 빌드 반복</li>
<li>PlayStore 내부 테스트를 이용하여 실제 배포와 동일하게 진행 → 스토어 반영 시간 (1~2시간) → 다운로드 → 테스트 진행</li>
</ul>
</li>
<li>테스트 완료 후 안드로이드 스튜디오 빌드 진행 → .apk / .aab 테스트폰에 각각 설치 후 최종 테스트</li>
<li>.aab(bundle) 파일을 가지고 스토어에 직접 수기로 심사 및 배포 진행<ul>
<li>.aab 파일이 이전버전이 아닌지 확인 해야하며, 관리 차원에서 휴먼 에러가 발생할 수 있다.</li>
</ul>
</li>
</ul>
<h3 id="1-테스트앱을-제공하는데-너무-많은-시간이-걸린다">1. 테스트앱을 제공하는데 너무 많은 시간이 걸린다.</h3>
<p>개발이 완료되면 앱을 빌드한뒤 업로드해야합니다. 우선 PlayStore에 2차 인증을 거친 로그인을 해야하고 , 적당한 릴리즈노트와 함께 앱을 업로드하면, 앱파일을 첨부하는데 일정 시간을 소요한뒤에 앱을 제출할 수 있습니다. 업로드가 완료되면 바로 제공되는 것도 아닙니다. 약 30분의 &#39;출시준비중&#39; 단계를 대기한 후에 테스터들에게 앱이 제공됩니다.</p>
<h3 id="2-앱이-제공되고나면-qa엔지니어에게-직접-알려야한다">2. 앱이 제공되고나면 QA엔지니어에게 직접 알려야한다.</h3>
<ul>
<li>앱이 제공되고나면 QA엔지니어에겐 누가 알려줄까요?<ul>
<li>개발자가 직접 구두 OR 채널</li>
</ul>
</li>
<li>앱 업로드 후 30분~1시간 가량 &#39;출시준비중&#39; 상태를 뺑글뺑글 도는 것을 지켜보다가, 출시가 완료되면 QA엔지니어에게 알려주어야 합니다.</li>
</ul>
<h3 id="3-테스터가-추가될때마다-일일이-추가해야하고-오랫동안-기다려야한다">3. 테스터가 추가될때마다 일일이 추가해야하고, 오랫동안 기다려야한다.</h3>
<p>신규 테스터를 추가하기 위해서는 </p>
<p>(1) 플레이스토어 계정을 전달받아야하고 </p>
<p>(2) 전달받은 계정에 수기로 권한을 등록해야합니다. </p>
<p>(3) 테스터가 정상적으로 권한을 획득했는지 확인해야하며,</p>
<p>(4) 권한을 획득한 테스터는 플레이스토어에서 &#39;베타 참여하기&#39; 버튼을 누른 뒤 </p>
<p>(5) 약 1~2 시간을 많게는 3시간 이상을 기다려야 테스트앱을 다운받을 수 있습니다.</p>
<h3 id="4-이전-버전을-테스트할-수-없다-한번에-1개의-버전만-테스트가-가능하다-os별-테스트시">4. 이전 버전을 테스트할 수 없다. 한번에 1개의 버전만 테스트가 가능하다. <code>(OS별 테스트시)</code></h3>
<p>그나마 TestFlight는 &#39;이전 빌드 보기&#39;통해 과거 버전의 앱도 설치할 수 있어서, iOS는 이 방법으로 기능별 버전을 분리해서 QA를 진행할 수 있습니다. 그러나 플레이스토어는 가장 최신 버전의 앱만 제공할 수 있어 동시다발적으로 여러개의 QA를 태울땐 항상 난감합니다. 또 프로덕트에 앱이 출시되고나면, 자연스레 Beta에 등록된 앱이 이전 버전이 되면서 무효화 되기 때문에 Beta앱을 또 다시 빌드해서 등록해주어야합니다. <strong>이는 배포역시 또 하나의 테스크로 변질되어진다.</strong></p>
<aside>
💡 새로운 방식 도입 (R&D)

</aside>

<blockquote>
<p>아래의 과정이 명령어 한줄이면 가능하다.</p>
</blockquote>
<ol>
<li>Fastlane(Build)</li>
<li>Firebase App Distribution(테스트 모드) / 슬랙 알림 (테스트모드)</li>
<li>테스트 진행 </li>
<li>Main PR (merge) 완료 → 심사 &amp; 배포 진행 → 슬랙 알림 (릴리즈 모드) </li>
<li>완료</li>
</ol>
<h2 id="1-fastlane-에서-명령어로-빌드를-진행한다">1. Fastlane 에서 명령어로 빌드를 진행한다.</h2>
<ul>
<li>명령어와 Fastlane 환경 셋팅에 따라 .dev / .stage / .release 로 활용 가능하다.</li>
</ul>
<h2 id="2-테스트-기기에서-전달-받은-초대-링크를-받는다--최초-1회-">2. 테스트 기기에서 전달 받은 초대 링크를 받는다 ( 최초 1회 )</h2>
<ul>
<li>앱을 다운받아 테스트 진행 ( Firebase 에서 다운받는 형식 )</li>
<li>슬랙을 통하여 알림 ( 이부분은 슬랙 환경 설정에 따라 릴리즈에서만 채널에 알림을 보내는것도 가능 )</li>
</ul>
<h2 id="3-테스트-완료-후-main-pr-완료-되면-playstore로-심사-등록-진행">3. 테스트 완료 후 Main PR 완료 되면 PlayStore로 심사 등록 진행</h2>
<h3 id="필수-등록-사항-리스트">필수 등록 사항 리스트</h3>
<ol>
<li>Google Cloude Platform 설정 ( Firebase와 PlayStore 앱 연결 을 위하여 셋팅 )</li>
<li>Firebase App Distributuin 설정<ol>
<li>테스터 설정 ( 이메일 초대 )</li>
</ol>
</li>
<li>Fastlane 환경 설정 셋팅</li>
<li>JAVA Version Upgrade (1.7 → 17)</li>
</ol>
<hr>
<h3 id="참고-기술-블로그">참고 기술 블로그</h3>
<p><a href="https://seosh817.tistory.com/332">[Android] Fastlane으로 App Distribution &amp; PlayStore에 배포하기</a></p>
<p><a href="https://sungbin.land/fastlane-firebase-app-distribution-ff57c15793a4"></a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Compose] Compsoe 연구개발 #2]]></title>
            <link>https://velog.io/@kk_jang93/Compose-Compsoe-%EC%97%B0%EA%B5%AC%EA%B0%9C%EB%B0%9C-2</link>
            <guid>https://velog.io/@kk_jang93/Compose-Compsoe-%EC%97%B0%EA%B5%AC%EA%B0%9C%EB%B0%9C-2</guid>
            <pubDate>Fri, 12 Jul 2024 05:32:52 GMT</pubDate>
            <description><![CDATA[<h1 id="읽기전에-알아야할-개념"><strong>읽기전에 알아야할 개념</strong></h1>
<h1 id="composition-recomposition"><strong>Composition, ReComposition</strong></h1>
<p>위 링크에서 언급한대로 Composition은 UI를 기술하는 컴포저블의 트리 구조입니다.</p>
<p>Compose 최초 Composition 시의 순서입니다.</p>
<p>1) 처음으로 컴포저블을 실행</p>
<p>2) UI를 기술하기 위해 호출하는 컴포저블을 추적</p>
<p>3) 앱 상태가 변경되면 Jetpack Compose는 recomposition을 예약</p>
<p>4) recompostion은 Compose가 상태 변경사항에 따라 Composable을 다시 실행해서 컴포지션을 업데이트</p>
<ul>
<li><strong>composition은 초기 composition을 통해서만 생성, recomposition을 통해서만 업데이트 가능*</strong></li>
</ul>
<h1 id="1-composable-lifecycle"><strong>1. Composable Lifecycle</strong></h1>
<p>Composable의 라이프 사이클은</p>
<p><strong>컴포지션 시작, 0회 이상 recomposition 및 컴포지션 종료 이벤트로 정의</strong></p>
<p><img src="https://velog.velcdn.com/images/kk_jang93/post/f956fce4-382e-4359-8be6-6f2dabc2459d/image.png" alt=""></p>
<p><strong>순서:</strong> Composable이 Composition을 시작 -&gt;</p>
<p>0회 이상 recomposition -&gt;</p>
<p>comosition을 종료</p>
<h3 id="1"><strong>1)</strong></h3>
<p>Composable 의 Lifecycle는 View, Activity 및 Fragment의 Lifecycle보다 간단합니다.</p>
<p>Composable 은 Lifecycle 이 더 복잡한 외부 리소스를 관리하거나 이와 상호작용해야 하는 경우 효과를 사용 해야함</p>
<h3 id="2"><strong>2)</strong></h3>
<p>Composable이 여러 번 호출되면 Composition에 여러 인스턴스가 배치됩니다.</p>
<p>Composition 각 호출에는 자체 Lifecycle가 있습니다.</p>
<pre><code class="language-kotlin">@Composable
fun MyComposable() {
    Column {
        Text(&quot;Hello&quot;)
        Text(&quot;World&quot;)
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/kk_jang93/post/c5620e7e-636c-4608-a9a2-dea7570d5718/image.png" alt=""></p>
<h3 id="3"><strong>3)</strong></h3>
<p>recomposition은 일반적으로 <strong>State<T></strong> 객체가 변경되면 트리거됩니다.</p>
<p>Compose는 이러한 객체를 추적합니다.</p>
<p><strong>아래 두개중에 건너뛸 수 없는 모든 컴포저블을 실행</strong></p>
<p>(1)Composition에서 특정 State<T>를 읽는 모든 Composable</p>
<p>(2)호출하는 Composable</p>
<h2 id="2-컴포지션-내-컴포저블의-분석"><strong>2. 컴포지션 내 컴포저블의 분석</strong></h2>
<ul>
<li><strong>호출 사이트</strong>는 컴포저블이 호출되는 소스 코드 위치*</li>
<li>Composition 내 composable 의 인스턴스는 <strong>호출 사이트</strong>로 식별</li>
<li>Compose 컴파일러는 각 호출 사이트를 고유한 것으로 간주</li>
<li>여러 호출 사이트에서 컴포저블을 호출하면 컴포지션에 컴포저블의 여러 인스턴스가 생성</li>
</ul>
<h3 id=""></h3>
<h3 id="하지만"><strong>하지만..</strong></h3>
<h3 id="recompostion-시-composable이-이전-recomposition-시-호출한-것과-다른-composable을-호출하는-경우"><strong>recompostion 시 composable이 이전 recomposition 시 호출한 것과 다른 composable을 호출하는 경우</strong></h3>
<p>1) Compose는 <strong>호출되거나 호출되지 않은 composable을 식별</strong></p>
<p>2) 두 Compostion 모두에서 호출된 composable의 경우 <strong>입력이 변경되지 않은 경우 recomposition 하지않음</strong></p>
<p>부수 효과를 composable과 연결하기 위해서는 ID를 유지하는 것이 중요함</p>
<p>(recomposition마다 다시 시작하는 대신 완료할 수 있도록)</p>
<p><strong>예제</strong></p>
<pre><code class="language-kotlin">@Composable
fun LoginScreen(showError: Boolean) {
    if (showError) {
        LoginError()
    }
    LoginInput() // This call site affects where LoginInput is placed in Composition
}

@Composable
fun LoginInput() { /* ... */ }</code></pre>
<p>위 예제에서 LoginScreen은 LoginError Composable을 조건부로 호출하며</p>
<p>항상 LoginInput Composable을 호출합니다.</p>
<p>각 호출에는 고유한 호출 사이트 및 컴파일러가 호출을 고유하게 식별하는 데</p>
<p>사용하는 소스 위치가 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/kk_jang93/post/f9076299-e436-449a-bd26-d69b0ef769f4/image.png" alt=""></p>
<p>상태가 변경되고 recomposition이 발생할 때 composition 내 LoginScreen의 표현.</p>
<p>(색상이 동일하면 recomposition되지 않았음을 의미합니다.)</p>
<p><strong>LoginInput이 첫 번째로 호출되었다가 두 번째로 호출되었지만</strong></p>
<p><strong>LoginInput 인스턴스는 여러 recomposition에 걸쳐 유지됩니다.</strong></p>
<p><strong>또한 LoginInput에는 recomposition 간에 변경된 매개변수가 없으므로</strong></p>
<p><strong>Compose가 LoginInput 호출을 건너뜁니다.</strong></p>
<h3 id="-1"></h3>
<h2 id="2-1스마트-recomposition에-도움이-되는-정보-추가"><strong>2-1.스마트 recomposition에 도움이 되는 정보 추가</strong></h2>
<ul>
<li>동일한 호출 사이트에서 composable을 여러 번 호출하는 경우 Compose가 인스턴스를 구분하기 위해 호출 사이트 외에 실행 순서가 사용됨</li>
<li>호출 사이트 외에 실행순서가 필요한 경우도 있지만 경우에 따라 원치 않는 동작이 발생할 수 있음</li>
</ul>
<p><strong>예제.</strong></p>
<pre><code class="language-kotlin">@Composable
fun MoviesScreen(movies: List&lt;Movie&gt;) {
    Column {
        for (movie in movies) {
            // MovieOverview composables are placed in Composition given its
            // index position in the for loop
            MovieOverview(movie)
        }
    }
}</code></pre>
<p>위 예에서 <strong>Compose는 호출 사이트 외에 실행 순서를 사용하여</strong></p>
<p><strong>Composition 에서 인스턴스를 구분합니다.</strong></p>
<p>새 movie가 <strong>리스트의 하단에 추가된 경우</strong></p>
<p>ex: list.add(movie)</p>
<p><strong>Compose는 인스턴스의 리스트내</strong></p>
<p><strong>위치가 변경되지 않았고</strong></p>
<p><strong>인스턴스의 movie 입력이 동일하므로</strong></p>
<p><strong>컴포지션에 이미 있는 인스턴스를 재사용할 수 있습니다.</strong></p>
<p>(ex: {1,2,3}일때 list.add(4) 를 하면  {1,2,3,4} 라서</p>
<p>이전과 비교해서 4를 제외한 1,2,3 값과 index 위치가 그대로 있는거라면 )</p>
<p><img src="https://velog.velcdn.com/images/kk_jang93/post/d4ee58ad-21af-4c3d-bc09-eb81de8fc08a/image.png" alt=""></p>
<p>위 그림처럼 composition 의 MovieOverview Composable은 재사용이 됩니다.</p>
<p>(색상이 동일하면 recomposition되지 않았음을 의미합니다.)</p>
<h3 id="-2"></h3>
<h3 id="-3"></h3>
<h3 id="-4"></h3>
<h3 id="하지만-1"><strong>하지만....</strong></h3>
<p><strong>리스트의 상단 아이템 추가, 리스트의 가운데 아이템 추가, 항목을 삭제, 재정렬</strong> 등등</p>
<p><strong>movies 목록이 변경되면 목록에서 입력 매개변수의 위치가</strong></p>
<p><strong>변경된 모든 MovieOverview Composable 호출에서 recomposition이 발생합니다.</strong></p>
<p>예를 들어 MovieOverview가 부수 효과를 사용하여 네트워크에서 이미지를 가져오는 경우 매우 중요합니다.</p>
<p>효과가 적용되는 동안 recomposition 이 발생하면 효과가 취소되고 다시 시작됩니다.</p>
<p><strong>예제</strong></p>
<pre><code class="language-kotlin">@Composable
fun MovieOverview(movie: Movie) {
    Column {
        // Side effect explained later in the docs. If MovieOverview
        // recomposes, while fetching the image is in progress,
        // it is cancelled and restarted.
        val image = loadNetworkImage(movie.url)
        MovieHeader(image)

        /* ... */
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/kk_jang93/post/07690d8d-5381-46d7-b8e7-dce9216a4eab/image.png" alt=""></p>
<p>위 그림은 ex:list.add(0,movie) 를 했을때의 예시입니다.</p>
<p>(색상이 동일하지 않으면 recomposition 되었음을 의미합니다.)</p>
<ul>
<li>재사용 없이 다시 recompostion이 됨</li>
<li>모든 부수효과가 다시 시작됨</li>
</ul>
<p><strong>그렇다면 위와 상황에서 recomposition 없이 재사용을 사용할려면</strong></p>
<p><strong>이상적으로 MovieOverview 인스턴스의 ID는</strong></p>
<p><strong>인스턴스에 전달된 movie의 ID에 연결되는것이 좋습니다.</strong></p>
<p>Compose에서 런타임에 트리의 특정 부분을</p>
<p>식별하는 데 사용할 <strong>Key</strong>를 지정할 수 있습니다.</p>
<p><strong>key 값은 전체적으로 고유하지 않아도 되며</strong></p>
<p><strong>호출 사이트에서의 Composable 호출 간에만 고유하면 됩니다.</strong></p>
<p>(앱의 다른 위치에 있는 다른 Composable과 이 key를 공유하는 것은 괜찮습니다.)</p>
<p><strong>예제</strong></p>
<pre><code class="language-kotlin">@Composable
fun MoviesScreen(movies: List&lt;Movie&gt;) {
    Column {
        for (movie in movies) {
            key(movie.id) { // Unique ID for this movie
                MovieOverview(movie)
            }
        }
    }
}</code></pre>
<p>movies의 요소가 변경되더라도 Compose는</p>
<p>key별로 MovieOverview() 호출을 인식하고 재사용할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/kk_jang93/post/4d4b9381-b14e-4942-9962-63a55d24f41d/image.png" alt=""></p>
<p>Key를 사용했기 때문에 고유 식별자가 생겨서 recomposition 없이 재사용이 가능해집니다.</p>
<p>(색상이 동일하면 recomposition되지 않았음을 의미합니다.)</p>
<p>일부 Composable에는 key Composable 지원 기능이 내장되어 있습니다.</p>
<p><strong>예제</strong></p>
<pre><code class="language-kotlin">@Composable
fun MoviesScreen(movies: List&lt;Movie&gt;) {
    LazyColumn {
        items(movies, key = { movie -&gt; movie.id }) { movie -&gt;
            MovieOverview(movie)
        }
    }
}
</code></pre>
<p>예를 들어 LazyColumn의 경우 items DSL에 맞춤 key를 지정할 수 있습니다.</p>
<h3 id="2-2입력이-변경되지-않은-경우-건너뛰기"><strong>2-2.입력이 변경되지 않은 경우 건너뛰기</strong></h3>
<p><img src="https://velog.velcdn.com/images/kk_jang93/post/55e96f2b-774b-45da-9694-c9c87c2cfb75/image.jpeg" alt=""></p>
<p><strong>Composition에 이미 Composable이 있는 경우 모든 input이 안정적이고 변경되지 않았으면</strong></p>
<p><strong>recompostion을 건너뛸 수 있습니다.</strong></p>
<p>Stable 타입은 다음을 준수해야 합니다.</p>
<p>1) 두 인스턴스의 equals 결과가 동일한 두 인스턴스의 경우 항상 동일</p>
<p>2) 타입의 Public 속성이 변경되면 Composition에 알림이 전송</p>
<p>3) 모든 Public 속성 유형도 안정적</p>
<p><strong>예외로 안정적이지만 변경할 수 있는 한 가지 중요한 타입은 Compose의 MutableState</strong></p>
<p>value가 MutableState로 유지되는 경우 State의 .value 속성이 변경되면 Compose에 알림이 전송합니다.</p>
<p>그래서 상태 객체는 안정적인 것으로 간주됩니다.</p>
<p>Composable에 매개변수로 전달된 모든 타입이이 안정적인 경우</p>
<p>UI 트리 내 Composition 위치를 기반으로 매개변수 값이 동일한지 비교합니다.</p>
<p>(이전 호출 이후 모든 값이 변경되지 않은 경우 recompostion 을 건너뜀)</p>
<h3 id="stable"><strong>@Stable</strong></h3>
<p>Compose는 증명할 수 있는 경우에만 유형을 안정적인 것으로 취급</p>
<p>(ex: 인터페이스는 일반적으로 안정적이지 않은 것 취급되며 구현을</p>
<p>변경할 수 없고 변경할 수 있는  속성이 있는 유형도 안정적이지 않음)</p>
<p>Compose가 유형이 안정적이라고 추론할 수 없을때,</p>
<p>안정적인 것으로 간주하도록 하려면 <a href="https://developer.android.com/reference/kotlin/androidx/compose/runtime/Stable">@Stable</a> 주석으로 표시하면 됨</p>
<p><strong>예제.</strong></p>
<pre><code class="language-kotlin">// Marking the type as stable to favor skipping and smart recompositions.
@Stable
interface UiState&lt;T : Result&lt;T&gt;&gt; {
    val value: T?
    val exception: Throwable?

    val hasError: Boolean
        get() = exception != null
}</code></pre>
<p>인터페이스는 이 타입을 안정적이지 않은 것으로 간주함</p>
<p><strong>하지만 @Stable 주석을 추가하면 Compose가 이 유형이 안정적임을 알게 되고 스마트 recomposition을</strong></p>
<p><strong>선호하게 됨</strong></p>
<p>인터페이스가 매개변수 유형으로 사용되는 경우</p>
<p>Compose가 모든 구현을 안정적인 것으로 간주함</p>
<p><strong>@Stable 주석을 사용하여 안정적이라고 명시되지 않더라도</strong></p>
<p><strong>Compose 컴파일러가 안정적인 것으로 간주하며 이 계약에 포함되는 중요한 일반 타입이 있습니다.</strong></p>
<p>1) 모든 원시 값 타입: Boolean, Int, Long, Float, Char 등</p>
<p>2) 문자열</p>
<p>3) 모든 함수 타입(람다)</p>
<ul>
<li>이 타입은 모두 변경할 수 없으므로 stable 계약을 준수함</li>
<li>변경할 수 없는 유형은 절대 변경되지 않으므로 Composition에 변경사항을 알리지 않아도 됨</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Compose] Compose 분석]]></title>
            <link>https://velog.io/@kk_jang93/Compose-Compose-%EB%B6%84%EC%84%9D</link>
            <guid>https://velog.io/@kk_jang93/Compose-Compose-%EB%B6%84%EC%84%9D</guid>
            <pubDate>Fri, 12 Jul 2024 05:24:46 GMT</pubDate>
            <description><![CDATA[<h1 id="compose-도입-배경">Compose 도입 배경</h1>
<aside>
💡 Jetpack Compose 도입 배경

</aside>

<p>Jetpack Compose 도입 배경에는 여러 가지 이유가 있습니다. 안드로이드 개발팀과 커뮤니티는 기존의 UI 개발 방식에서 나타나는 문제들을 해결하고, 더 나은 개발 경험을 제공하기 위해 Compose를 설계했습니다. 주요 도입 배경은 다음과 같습니다</p>
<h3 id="1-기존-xml-기반-ui의-복잡성"><strong>1. 기존 XML 기반 UI의 복잡성</strong></h3>
<p>XML 기반 UI 개발은 다음과 같은 단점이 있습니다:</p>
<ul>
<li><strong>이원화된 코드 구조</strong>: UI 레이아웃은 XML 파일에서 정의되고, 동작은 Kotlin/Java 파일에서 정의되어야 합니다. 이는 코드의 일관성을 떨어뜨리고 유지보수를 어렵게 만듭니다.</li>
<li><strong>복잡한 상태 관리</strong>: UI 컴포넌트의 상태를 변경하고 업데이트하는 과정이 복잡하며, 종종 버그를 유발합니다.</li>
<li><strong>보일러플레이트 코드</strong>: 많은 양의 보일러플레이트 코드가 필요하여 생산성이 저하됩니다.</li>
</ul>
<h3 id="2-현대적인-ui-개발-패러다임"><strong>2. 현대적인 UI 개발 패러다임</strong></h3>
<p>기존의 명령형 프로그래밍 방식 대신 선언형 프로그래밍 방식이 각광받기 시작하면서, React(웹 개발)와 Flutter(크로스플랫폼 모바일 개발)와 같은 프레임워크들이 인기를 끌었습니다. 이러한 트렌드에 맞추어 안드로이드 개발팀도 Jetpack Compose를 도입하게 되었습니다:</p>
<ul>
<li><strong>선언형 프로그래밍</strong>: UI 상태를 기반으로 UI를 선언적으로 정의하여, 코드의 가독성과 유지보수성을 높입니다.</li>
<li><strong>반응형 UI 업데이트</strong>: 상태 변경에 따라 자동으로 UI를 업데이트하여 개발자가 상태와 UI 동기화를 일일이 관리할 필요가 없습니다.</li>
</ul>
<h3 id="3-개발-생산성-향상"><strong>3. 개발 생산성 향상</strong></h3>
<p>Jetpack Compose는 개발자의 생산성을 높이기 위해 다음과 같은 기능을 제공합니다:</p>
<ul>
<li><strong>간결한 코드</strong>: XML과 뷰 바인딩 코드를 없애고, 하나의 Kotlin 파일에서 모든 UI를 정의할 수 있습니다.</li>
<li><strong>재사용 가능한 컴포넌트</strong>: Composable 함수를 사용하여 UI 컴포넌트를 모듈화하고 재사용성을 극대화합니다.</li>
<li><strong>강력한 툴링 지원</strong>: Android Studio는 Compose를 위한 실시간 미리보기, 코드 완성, 리팩토링 도구 등을 제공하여 개발 효율성을 높입니다.</li>
</ul>
<h3 id="4-성능-개선"><strong>4. 성능 개선</strong></h3>
<p>Jetpack Compose는 성능 면에서도 이점을 제공합니다:</p>
<ul>
<li><strong>효율적인 리컴포지션</strong>: 상태 변경 시 필요한 부분만 다시 렌더링하여 성능을 최적화합니다.</li>
<li><strong>최소화된 메모리 사용</strong>: 필요한 UI 컴포넌트만 메모리에 로드하여 메모리 사용량을 줄입니다.</li>
</ul>
<h3 id="5-커뮤니티와-생태계의-발전"><strong>5. 커뮤니티와 생태계의 발전</strong></h3>
<p>Jetpack Compose는 구글의 지원 아래 적극적으로 발전하고 있으며, 커뮤니티의 피드백을 반영하여 지속적으로 개선되고 있습니다. 이는 안드로이드 개발 생태계를 더욱 강화하고, 개발자들이 최신 기술을 사용할 수 있도록 돕습니다.</p>
<h3 id="결론">결론</h3>
<ul>
<li>Jetpack Compose 도입 배경은 기존의 XML 기반 UI 개발 방식의 단점을 극복하고, 선언형 프로그래밍 패러다임을 통해 더 나은 개발 생산성과 성능을 제공하기 위함입니다. 현대적인 UI 개발 트렌드에 맞추어, 안드로이드 개발자들이 더 간편하고 효율적으로 UI를 작성하고 유지보수할 수 있도록 설계된 것이 Compose의 주요 도입 배경입니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Rabbit MQ 연구개발]]></title>
            <link>https://velog.io/@kk_jang93/Rabbit-MQ-%EC%97%B0%EA%B5%AC%EA%B0%9C%EB%B0%9C</link>
            <guid>https://velog.io/@kk_jang93/Rabbit-MQ-%EC%97%B0%EA%B5%AC%EA%B0%9C%EB%B0%9C</guid>
            <pubDate>Fri, 12 Jul 2024 05:22:33 GMT</pubDate>
            <description><![CDATA[<h3 id="1-rabbitmq-download">1. RabbitMQ Download</h3>
<pre><code class="language-jsx">$ brew update
$ brew install rabbitmq
$ ~/.zshrc

* path 입력 후 저장
$ export PATH=$PATH:/usr/local/sbin
$ source ~/.zsrhc

해당 경로로 진입하여 rabbitmq 서버를 로컬로 실행한다.
$ cd /opt/homebrew/opt/rabbitmq/sbin/
$ ./rabbitmq-server

서버 실행이 에러날 경우
ERROR: could not bind to distribution port 25672, it is in use by another node: rabbit@localhost

$ sudo lsof -i :25672 ( 해당 명령어 실행 후 pid 확인 )
$ sudo kill &lt;PID&gt;
$ ./rabbitmq-server

에러 발생시 서버를 죽이고 다시 실행한다.
</code></pre>
<p>아래 사진은 ./ rabbitmq-server 명령어 실행시 완료 화면</p>
<p><img src="https://velog.velcdn.com/images/kk_jang93/post/0b6a7717-4c96-42a5-8612-98cc3351c64c/image.png" alt=""></p>
<pre><code class="language-jsx">chrom 에서 해당 로컬서버로 http://localhost:15672 접속

id : guest
pw : guest
처음엔 default 인 guest / guest 으로 되어있음

추후 계정 생성시 아래 명령어 참고
./rabbitmqctl add_user rabbitmq {id} {pw} -- 계정 생성
./rabbitmqctl set_user_tags {id} administrator -- 계정 어드민 권한 부여

$ expo init client
$ npm install amqplib
$ cd client

* 테스트용 프로젝트
Project_Name : client

$ cd /Users/{name}/Desktop/client/src
$ ls
$ sendMessage.js / receiveMessage.js

메시지 보낼때 
$ node sendMessage.js -- 실행

메시지 받을때
$ node receiveMessage.js -- 실행
</code></pre>
<blockquote>
<p>해당 경로 참고</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/kk_jang93/post/94284a85-90f4-44b6-8d7a-94734b1bdcf1/image.png" alt=""></p>
<pre><code class="language-jsx">sendMessage.js

/*
 * npm install amqplib
 */

const { execSync } = require(&quot;child_process&quot;);
const amqp = require(&#39;amqplib/callback_api&#39;);

// amqp 예시
//amqp://admin:admin@localhost admin:admin = rabbitmq 계정:암호

const prod = &#39;amqp://{name}:vlTmRun1214!@101.79.9.43:5672&#39;;
const local = &#39;amqp://guest:guest@localhost&#39;;

amqp.connect(local, function (error0, connection) {
    if (error0) {
        throw error0;
    }

    connection.createChannel(function (error1, channel) {
        if (error1) {
            throw error1;
        }

        //queue name
        var queue = &#39;test&#39;;

        /*
        * queue가 없으면 만들어줌
        * durable : true -&gt; queue 데이터를  rabbitmq가 재시작해도 가지고 있음(소비하기전까지)
        */
        channel.assertQueue(queue, {
            durable: false,
            exclusive: false,
            autoDelete: false,
            arguments: null,
        });
        setInterval(sendToQueue, 1000, channel, queue)
    });

    setTimeout(function () {
        connection.close();
        process.exit(0);
    }, 10000);
});

function sendToQueue(channel, queue) {
       var msg = &#39;Hello World! transDate:&#39; + new Date();
       channel.sendToQueue(queue, Buffer.from(msg));

    //json 데이터 보내기 (receive 에서 아래 포맷형식으로 보내지 않으면 메시지를 받을 수 없다...)
    // var msg = &#39;{&quot;test&quot;:&quot;테스트 입니다&quot;, &quot;name&quot;:&quot;테스트에용&quot;, &quot;title&quot;:1111}&#39;;
    // channel.sendToQueue(queue, Buffer.from(JSON.stringify(msg)));

    console.log(&quot; [x] Sent %s&quot;, msg);
}</code></pre>
<pre><code class="language-jsx">receiveMessage.js

/*
 * npm install amqplib
 */
const { execSync } = require(&quot;child_process&quot;);
const amqp = require(&#39;amqplib/callback_api&#39;);

// amqp 예시
//amqp://admin:admin@localhost admin:admin = rabbitmq 계정:암호

const prod = &#39;amqp://piece:vlTmRun1214!@101.79.9.43:5672&#39;;
const local = &#39;amqp://guest:guest@localhost&#39;;

amqp.connect(local, function (error0, connection) {
    if (error0) {
        throw error0;
    }

    connection.createChannel(function (error1, channel) {
        if (error1) {
            throw error1;
        }

        //queue name
        var queue = &#39;test&#39;;

        /*
        * queue가 없으면 만들어줌
        * durable : true -&gt; queue 데이터를  rabbitmq가 재시작해도 가지고 있음(소비하기전까지)
        */
        channel.assertQueue(queue, {
            durable: false,
            exclusive: false,
            autoDelete: false,
            arguments: null,
        });

        console.log(&quot; [*] Waiting for messages in %s. To exit press CTRL+C&quot;, queue);

        //prefetch를 설정해두면 큐에서 최대 10개만 가져감.
        channel.prefetch(10);
        channel.consume(queue, function (msg) {
            console.log(&quot; [x] Received %s&quot;, msg.content.toString());

            // json 데이터 받기
            // var result = JSON.parse(msg.content.toString());
            // console.log(result.testVal1, result.testVal2, result.testVal3);

            //Ack 메세지를 보내야 큐에서 제거함
            channel.ack(msg);
            //channel.nack(msg);
        }, {
            //noAck: true 이면 queue에서 데이터를 가져간다음 Ack를 바로 반환함으로 가져가자마자 queue에서 지워버림, ack를 받았을 경우만 큐에서 제거하기 위해 false로 설정
            noAck: false
        });
    });
    setTimeout(function () {
        connection.close();
        process.exit(0);
    }, 10000);
});</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React-Native] Expo build 이미지 종속]]></title>
            <link>https://velog.io/@kk_jang93/React-Native-Expo-build-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%A2%85%EC%86%8D</link>
            <guid>https://velog.io/@kk_jang93/React-Native-Expo-build-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%A2%85%EC%86%8D</guid>
            <pubDate>Fri, 12 Jul 2024 05:19:01 GMT</pubDate>
            <description><![CDATA[<p>모든 이미지 자산 및 품질 손실 최소화 ( 자동 압축 처리 )</p>
<blockquote>
<p>$ npm install -g sharp-cli</p>
</blockquote>
<p>or</p>
<blockquote>
<p>$ npx expo-optimize [options]</p>
</blockquote>
<ul>
<li>options 
[ jpegoptim , guetzli , pngcrush , optipng , imagemin ]</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React-Native] Expo build jsEngine]]></title>
            <link>https://velog.io/@kk_jang93/React-Native-Expo-build-jsEngine</link>
            <guid>https://velog.io/@kk_jang93/React-Native-Expo-build-jsEngine</guid>
            <pubDate>Fri, 12 Jul 2024 05:17:12 GMT</pubDate>
            <description><![CDATA[<hr>
<pre><code class="language-jsx">1. App.js 추가

{
    &quot;expo&quot;: {
        &quot;jsEngine&quot;: &quot;hermes&quot;
    }
}

예시
{
  &quot;expo&quot;: {
    &quot;name&quot;: &quot;client&quot;,
    &quot;slug&quot;: &quot;client&quot;,
    **&quot;jsEngine&quot; : &quot;hermes&quot;, &lt;-- 추가**
    &quot;version&quot;: &quot;1.0.0&quot;,
    &quot;orientation&quot;: &quot;portrait&quot;,
    &quot;icon&quot;: &quot;./assets/icon.png&quot;,
    &quot;splash&quot;: {
      &quot;image&quot;: &quot;./assets/splash.png&quot;,
      &quot;resizeMode&quot;: &quot;contain&quot;,
      &quot;backgroundColor&quot;: &quot;#ffffff&quot;
    },
    &quot;updates&quot;: {
      &quot;fallbackToCacheTimeout&quot;: 0
    },
    &quot;assetBundlePatterns&quot;: [
      &quot;**/*&quot;
    ],
    &quot;ios&quot;: {
      &quot;bundleIdentifier&quot;: &quot;com.rabbit.testApp&quot;,
      &quot;supportsTablet&quot;: true,
      &quot;buildNumber&quot;: &quot;1.0.0&quot;
    },
    &quot;android&quot;: {
      &quot;package&quot;: &quot;com.rabbit.testApp&quot;,
      &quot;versionCode&quot;: 1 ,
      &quot;adaptiveIcon&quot;: {
        &quot;foregroundImage&quot;: &quot;./assets/adaptive-icon.png&quot;,
        &quot;backgroundColor&quot;: &quot;#FFFFFF&quot;
      }
    },
    &quot;web&quot;: {
      &quot;favicon&quot;: &quot;./assets/favicon.png&quot;
    }
  }
}

2. Command Insert
* 해당 프로젝트에 진입 후 아래 명령어 실행 

$ npm install -g eas-cli
$ npm install -g eas-cli-local-build-plugin
</code></pre>
<p>😮‍💨  M1 Mac</p>
<p><strong>iOS용 Hermes를 사용할 때 시뮬레이터용으로 빌드할 때 다음 오류가 발생할 수 있습니다.</strong></p>
<p><img src="blob:https://velog.io/6e734f6d-983b-47b6-9827-4946181258e7" alt="업로드중.."></p>
<pre><code class="language-jsx">--- a/ios/Podfile
+++ b/ios/Podfile
@@ -25,6 +25,22 @@ target &#39;HelloWorld&#39; do
   post_install do |installer|
     react_native_post_install(installer)

+    # Workaround simulator build error for hermes with react-native 0.64 on mac m1 devices
+    arm_value = `/usr/sbin/sysctl -n hw.optional.arm64 2&gt;&amp;1`.to_i
+    has_hermes = has_pod(installer, &#39;hermes-engine&#39;)
+    if arm_value == 1 &amp;&amp; has_hermes
+      projects = installer.aggregate_targets
+        .map{ |t| t.user_project }
+        .uniq{ |p| p.path }
+        .push(installer.pods_project)
+      projects.each do |project|
+        project.build_configurations.each do |config|
+          config.build_settings[&quot;EXCLUDED_ARCHS[sdk=iphonesimulator*]&quot;] = config.build_settings[&quot;EXCLUDED_ARCHS[sdk=iphonesimulator*]&quot;] + &#39; arm64&#39;
+        end
+        project.save()
+      end
+    end
+
     # Workaround `Cycle inside FBReactNativeSpec` error for react-native 0.64
     # Reference: https://github.com/software-mansion/react-native-screens/issues/842#issuecomment-812543933
     installer.pods_project.targets.each do |target|

// 해결 방법
Pod를 다시 설치하고 Xcode 빌드 캐시를 정리합니다.

$ npx pod-install
$ xcodebuild clean -workspace ios/{projectName}.xcworkspace -scheme {projectName}</code></pre>
<hr>
<p>Android build Test Result</p>
<hr>
<h1 id="최적화-전-빌드-진행">최적화 전 빌드 진행</h1>
<p><img src="https://velog.velcdn.com/images/kk_jang93/post/b7bd7468-eaf5-4c75-9864-cd9dc2e2191c/image.png" alt=""></p>
<h1 id="최적화-후-빌드-진행">최적화 후 빌드 진행</h1>
<p><img src="https://velog.velcdn.com/images/kk_jang93/post/e6d75548-167b-4073-a65c-d366c86b6bc7/image.png" alt="">
<img src="https://velog.velcdn.com/images/kk_jang93/post/e410207c-b2bc-4707-827f-7d4692c85aaf/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React-Native] Expo EAS Build]]></title>
            <link>https://velog.io/@kk_jang93/React-Native-Expo-EAS-Build</link>
            <guid>https://velog.io/@kk_jang93/React-Native-Expo-EAS-Build</guid>
            <pubDate>Fri, 12 Jul 2024 05:15:40 GMT</pubDate>
            <description><![CDATA[<h2 id="expo-eas-build">Expo EAS Build</h2>
<pre><code class="language-jsx">$ npm install -g eas-cli
$ npm install -g eas-cli-local-build-plugin

$ expo init &lt;test&gt;
$ cd &lt;test&gt;

$ eas login
Log in to EAS
Email or username : &lt;expo account id&gt;
Password : &lt;expo account pw&gt;
</code></pre>
<h1 id="easjson">eas.json</h1>
<pre><code class="language-jsx">{
  &quot;cli&quot;: {
    &quot;version&quot;: &quot;&gt;= 0.50.0&quot;
  },
  &quot;build&quot;: {
    &quot;development&quot;: {
      &quot;developmentClient&quot;: true,
      &quot;distribution&quot;: &quot;internal&quot;
    },
    &quot;preview&quot;: {
      &quot;distribution&quot;: &quot;internal&quot;
    },
    &quot;production&quot;: {}
  },
  &quot;submit&quot;: {
    &quot;production&quot;: {}
  }
}</code></pre>
<pre><code class="language-jsx">$ expo build:android
$ expo build:ios

$ expo build:android --type app-bundle
$ expo build:android -t app-bundle

아래 선택지중 택 1
&gt; npm install -g eas-cli
&gt; eas build -p android 
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React-Native] Expo 생체인증]]></title>
            <link>https://velog.io/@kk_jang93/React-Native-Expo-%EC%83%9D%EC%B2%B4%EC%9D%B8%EC%A6%9D</link>
            <guid>https://velog.io/@kk_jang93/React-Native-Expo-%EC%83%9D%EC%B2%B4%EC%9D%B8%EC%A6%9D</guid>
            <pubDate>Fri, 12 Jul 2024 05:13:05 GMT</pubDate>
            <description><![CDATA[<aside>
👀 Google Android 공식 가이드

</aside>

<pre><code class="language-jsx">https://developer.android.com/training/sign-in/biometric-auth?hl=ko

$ npm i --save react-native-touch-id

or

$ yarn add react-native-touch-id

===&gt; 해당 라이브러리 확인 결과 업데이트가 19년 이후로 없어서 Android 10 이상에서 지원 **불가**

===========================================================================

Android SDK 10 이상 지원 가능
Expo SDK 40 부터 지원 가능

$ expo install expo-local-authentication

// Expo 공식 문서 가이드
https://docs.expo.dev/versions/latest/sdk/local-authentication/

// Github 참고 가이드
https://github.com/expo/expo/tree/main/packages/expo-local-authentication

SecurityLevel {
    NONE ,      # 등록된 지문 없음
    SECRET ,    # 비 생체 인증 (ex. PIN , 패턴 )
    BIOMETRIC   # 생체 인증
}
NONE = 0 , SECRET = 1 , BIOMETRIC = 2

AuthenticationType { 
    FINGERPRINT ,      # 지문
    RECOGNITION ,      # 얼굴인식
    IRIS (Android 전용) # 홍채인식
}

FINGERPRINT = 1 , RECOGNITION = 2 , IRIS = 3

// 생체인증 참고 레퍼런스
https://dev.to/bionicjulia/biometric-authentication-in-react-native-with-expo-3l20

// 최근 업데이트 기준 npm
https://www.npmjs.com/package/expo-local-authentication

// React-Native Auth 참고 레퍼런스
https://medium.com/swlh/how-to-use-face-id-with-react-native-or-expo-134231a25fe4
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React-Native] Expo Build]]></title>
            <link>https://velog.io/@kk_jang93/React-Native-Expo-Build</link>
            <guid>https://velog.io/@kk_jang93/React-Native-Expo-Build</guid>
            <pubDate>Fri, 12 Jul 2024 05:10:43 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이전에 공부하였던 자료를 옮겨 놓기 위하여 기록합니다.</p>
</blockquote>
<h2 id="react-native-expo-build">React-Native Expo Build</h2>
<pre><code>로컬 개발 환경 버전 (Dev .ver)

MacOS Monterey 12.3.1
Xcode 13.2.1

brew 3.4.6
node 16.6.2
npm 7.20.3
watchman 2022.03.21.00
expo-cli 5.3.1
pod 1.11.3
openjdk 11.0.14.1
javac 11.0.14.1
</code></pre><h1 id="🧭-개발-환경-가이드">🧭 개발 환경 가이드</h1>
<aside>
👀 Homebrew ( Mac M1 )

</aside>

<p><a href="https://velog.io/@tycode4/M1-Homebrew%EC%84%A4%EC%B9%98">M1: Homebrew 설치</a></p>
<aside>
💡 **1. terminal 입력**

<pre><code class="language-jsx">$ /bin/bash -c 
&quot;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)&quot;

$ brew
$ zsh:command not found: brew
M1 의 경로는 /usr/local/.. 이 아닌 /opt/homebrew/.. 로 설정되어있음. 확인</code></pre>
<p><strong>2. 환경 변수 추가</strong></p>
<pre><code># If you come from bash you might have to change your $PATH.
# export PATH=$HOME/bin:/usr/local/bin:$PATH


# Path to your oh-my-zsh installation.
export ZSH=&quot;$HOME/.oh-my-zsh&quot;

# Set name of the theme to load --- if set to &quot;random&quot;, it will
# load a random theme each time oh-my-zsh is loaded, in which case,

# to know which specific one was loaded, run: echo $RANDOM_THEME
# See https://github.com/ohmyzsh/ohmyzsh/wiki/Themes
ZSH_THEME=&quot;robbyrussell&quot;



# Set list of themes to pick from when loading at random
# Setting this variable when ZSH_THEME=random will cause zsh to load
# a theme from this variable instead of looking in $ZSH/themes/
# If set to an empty array, this variable will have no effect.
# ZSH_THEME_RANDOM_CANDIDATES=( &quot;robbyrussell&quot; &quot;agnoster&quot; )

# Uncomment the following line to use case-sensitive completion.
# CASE_SENSITIVE=&quot;true&quot;

# Uncomment one of the following lines to change the auto-update behavior
# zstyle &#39;:omz:update&#39; mode reminder  # just remind me to update when it&#39;s time

# Uncomment the following line to change how often to auto-update (in days).
# zstyle &#39;:omz:update&#39; frequency 13

# Uncomment the following line if pasting URLs and other text is messed up.
# DISABLE_MAGIC_FUNCTIONS=&quot;true&quot;

# Uncomment the following line to disable colors in ls.
# DISABLE_LS_COLORS=&quot;true&quot;

# Uncomment the following line to disable auto-setting terminal title.
# DISABLE_AUTO_TITLE=&quot;true&quot;

# Uncomment the following line to enable command auto-correction.
# ENABLE_CORRECTION=&quot;true&quot;

# e.g. COMPLETION_WAITING_DOTS=&quot;%F{yellow}waiting...%f&quot;
# COMPLETION_WAITING_DOTS=&quot;true&quot;

# Uncomment the following line if you want to disable marking untracked files
# under VCS as dirty. This makes repository status check for large repositories
# much, much faster.
# DISABLE_UNTRACKED_FILES_DIRTY=&quot;true&quot;

# Uncomment the following line if you want to change the command execution time
# stamp shown in the history command output.
# You can set one of the optional three formats:
# &quot;mm/dd/yyyy&quot;|&quot;dd.mm.yyyy&quot;|&quot;yyyy-mm-dd&quot;
# or set a custom format using the strftime function format specifications,
# see &#39;man strftime&#39; for details.
# HIST_STAMPS=&quot;mm/dd/yyyy&quot;

# Would you like to use another custom folder than $ZSH/custom?
# ZSH_CUSTOM=/path/to/new-custom-folder

# Which plugins would you like to load?
# Standard plugins can be found in $ZSH/plugins/
# Custom plugins may be added to $ZSH_CUSTOM/plugins/
# Example format: plugins=(rails git textmate ruby lighthouse)
# Add wisely, as too many plugins slow down shell startup.
plugins=(git)

source $ZSH/oh-my-zsh.sh

# User configuration

# export MANPATH=&quot;/usr/local/man:$MANPATH&quot;

# You may need to manually set your language environment
# export LANG=en_US.UTF-8

# Preferred editor for local and remote sessions
# if [[ -n $SSH_CONNECTION ]]; then
#   export EDITOR=&#39;vim&#39;
# fi

# Compilation flags
# export ARCHFLAGS=&quot;-arch x86_64&quot;

# Set personal aliases, overriding those provided by oh-my-zsh libs,
# plugins, and themes. Aliases can be placed here, though oh-my-zsh
# users are encouraged to define aliases within the ZSH_CUSTOM folder.
# For a full list of active aliases, run `alias`.
#
# Example aliases
# alias zshconfig=&quot;mate ~/.zshrc&quot;
# alias ohmyzsh=&quot;mate ~/.oh-my-zsh&quot;

# brew
export PATH=/opt/homebrew/bin:$PATH
eval $(/opt/homebrew/bin/brew shellenv)

# NVM
export NVM_DIR=&quot;$HOME/.nvm&quot;
  [ -s &quot;/opt/homebrew/opt/nvm/nvm.sh&quot; ] &amp;&amp; . &quot;/opt/homebrew/opt/nvm/nvm.sh&quot;
 # This loads nvm
 # This loads nvm bash_completion

# GEM
export GEM_HOME=$HOME/.gem
export PATH=$GEM_HOME/bin:$PATH

# JDK
# export JAVA_HOME=$(/usr/libexec/java_home -v &#39;1.8*&#39;)
# export JAVA_HOME=/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home
export JAVA_HOME=/Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home
export PATH=${PATH}:$JAVA_HOME/bin


# AndroidHOME
export ANDROID_HOME=/Users/piecejhm/Library/Android/sdk
export PATH=$PATH:$ANDROID_HOME/emulator
export PATH=$PATH:$ANDROID_HOME/tools
export PATH=$PATH:$ANDROID_HOME/tools/bin
export PATH=$PATH:$ANDROID_HOME/platform-tools

# RabbitMQ
export PATH=$PATH:/opt/homebrew/opt/rabbitmq/sbin/


# VisualStudio
code () {
if [[ $# = 0 ]]
then
    open -a &quot;Visual Studio Code&quot;
else
    [[ $1 = /* ]] &amp;&amp; F=&quot;$1&quot; || F=&quot;$PWD/${1#./}&quot;
    open -a &quot;Visual Studio Code&quot; --args &quot;$F&quot;
fi
}</code></pre><pre><code class="language-jsx">$ source ~/.zshrc</code></pre>
<p>3<strong>. Cask install</strong></p>
<pre><code class="language-jsx">$ brew cask install google-chrome</code></pre>
<p>$ npm install -g expo-cli</p>
<p>$ expo init {ProjectName}</p>
<p>$ cd {ProjectName}</p>
<p>$ expo start 또는 npm start</p>
<h3 id="💥--react-native-설치-환경-가이드">💥  React-Native 설치 환경 가이드</h3>
<p>  <a href="http://ww25.qnrjs42.blog/react-native/m1-arm64-setting?subid1=20240712-1510-016a-80b7-d2f75389a7bd">http://ww25.qnrjs42.blog/react-native/m1-arm64-setting?subid1=20240712-1510-016a-80b7-d2f75389a7bd</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] Coroutine 파헤치기#2]]></title>
            <link>https://velog.io/@kk_jang93/Android-Coroutine-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B02</link>
            <guid>https://velog.io/@kk_jang93/Android-Coroutine-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B02</guid>
            <pubDate>Fri, 12 Jul 2024 04:58:54 GMT</pubDate>
            <description><![CDATA[<p>이전 글에 이어서 진행하겠습니다.</p>
<blockquote>
<p>코루틴의 Scope</p>
</blockquote>
<p>이제 특정 코루틴과 자식 코루틴을 어떻게 취소할지를 결정하는 2가지 Scope를 알아보려 합니다.</p>
<ul>
<li>CoroutineScope</li>
<li>SupervisorScope</li>
</ul>
<h3 id="coroutinescope">CoroutineScope</h3>
<p>아래에 일반적인 CoroutineScope 내부에 2개의 자식 코루틴을 실행하는 코드가 있습니다. 앱을 실행해볼까요?</p>
<pre><code>class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        CoroutineScope(Dispatchers.Main).launch {
            launch {
                delay(300L)
                throw Exception(&quot;Coroutine 1 failed&quot;)
            }
            launch {
                delay(400L)
                println(&quot;Coroutine 2 finished&quot;)
            }
        }
    }
}</code></pre><p><img src="https://velog.velcdn.com/images/kk_jang93/post/f944d93f-86ba-451a-a2e2-08dd67183296/image.png" alt=""></p>
<p>당연하게도 Coroutine 2가 완료되기도 전에 Coroutine 1에서 예외를 던지므로 앱에서 크래시가 발생하고 &quot;Coroutine 1 failed&quot;라는 에러를 확인할 수 있습니다.</p>
<p>그렇다면 CoroutineExceptionHandler를 사용하면 어떻게 될까요?</p>
<p>val handler = CoroutineExceptionHandler { _, throwable -&gt;
    println(&quot;Caught exception: $throwable&quot;)
}</p>
<pre><code>// + 연산자를 통해 2개의 CoroutineContext를 합쳐서 CoroutineScope에 적용
CoroutineScope(Dispatchers.Main + handler).launch {
    launch {
        delay(300L)
        throw Exception(&quot;Coroutine 1 failed&quot;)
    }
    launch {
        delay(400L)
        println(&quot;Coroutine 2 finished&quot;)
    }
}
</code></pre><p><img src="https://velog.velcdn.com/images/kk_jang93/post/8007c978-5c51-4a59-b206-67bab99c592f/image.png" alt=""></p>
<p>&quot;Caught exception: java.lang.Exception: Coroutine 1 failed&quot;에서 예외를 잡아 앱에 크래시가 발생하지 않았지만 Coroutine 2가 완료된 것을 확인할 수 없습니다.</p>
<p>위에서 2개의 자식 코루틴은 개별적으로 실행되는 것으로 보이는데 왜 Coroutine 1이 Coroutine 2에 영향을 끼친 것처럼 보일까요?</p>
<p>그 이유는 바로 CoroutineScope를 사용했기 때문입니다.</p>
<p>CoroutineScope는 예외를 처리했든 안했든 코루틴이 실패하자마자 모든 자식 코루틴과 전체 Scope를 취소합니다. 다시 정리하자면 CoroutineScope는 단 하나의 코루틴이 실패하더라도 스코프 전체가 취소됩니다. 여기서 실패(fail)는 예외를 던지는 것을 의미합니다.</p>
<p>여기서 다른 버전의 CoroutineScope인 SupervisorScope 개념이 등장합니다.</p>
<h2 id="supervisorscope">SupervisorScope</h2>
<p>그러면 위의 코드에서 2개의 launch 블록들을 supervisorScope 내부에 넣고 재실행 해봅시다.</p>
<pre><code>val handler = CoroutineExceptionHandler { _, throwable -&gt;
    println(&quot;Caught exception: $throwable&quot;)
}

CoroutineScope(Dispatchers.Main + handler).launch {
    supervisorScope {  // 자식 코루틴들을 supervisorScope 내부에 넣는다.
        launch {
            delay(300L)
            throw Exception(&quot;Coroutine 1 failed&quot;)
        }
        launch {
            delay(400L)
            println(&quot;Coroutine 2 finished&quot;)
        }
    }
}</code></pre><p>위의 코드를 실행해 보면 예외도 잡혔고 Coroutine 2도 완료된 것을 확인할 수 있습니다.</p>
<p>SupervisorScope는 내부의 코루틴 하나가 실패하거나 예외를 던지더라도 해당 Scope 내부의 다른 코루틴에게 영향을 주지 않습니다.</p>
<p>즉, 여러 개의 코루틴들을 묶어서 하나가 실패하면 모두 실패할지 아닐지에 대한 동작을 CoroutineScope 또는 SupervisorScope를 통해 정의할 수 있는 것입니다.</p>
<p>이 개념이 중요한 이유는 앱에서 커스텀한 CoroutineScope가 필요해지는 경우가 있기 때문입니다. 컴포넌트의 수명 주기를 관리하기 위해 고유한 CoroutineScope를 작성하여 해당 컴포넌트가 적절히 취소되어 더 이상 사용되지 않도록 위해서 말이죠.</p>
<p>viewModelScope가 그러한 scope의 예시입니다. ViewModel이 clear되면 해당 ViewModel 내에서 실행되는 모든 코루틴들 또한 clear됩니다.</p>
<p>이러한 동작을 하는 custom scope를 생성하기 위해서 많은 사람들이 CoroutineScope(Dispatchers.Main + handler)와 같은 형태의 코드를 사용합니다. 하지만 여기서 가장 큰 실수와 문제점은 자신의 custom scope에 대해 이러한 작업을 수행할 경우, 전체 컴포넌트에서 하나의 코루틴이 실패하면 다른 모든 것들 또한 실패하고 CoroutineScope가 취소된다는 것입니다. Scope가 한 번 취소되면 새로운 코루틴을 다시 실행할 수 없습니다.</p>
<p>이러한 상황이 viewModelScope에서 발생한다고 가정해봅시다. 예시로 하나의 네트워크 호출이 viewModelScope에서 실패하여 예외를 던진다면 내부의 다른 코루틴들도 모두 취소될 것이고, viewModelScope 전체도 취소되어 ViewModel 전체를 다시 생성하지 않는 한 새로운 코루틴을 시작할 수 없습니다. 이것은 우리가 ViewModel에서 원하는 동작이 아닐겁니다.</p>
<p><img src="https://velog.velcdn.com/images/kk_jang93/post/1e4b9caa-e159-43b7-8ce3-a40d63003400/image.png" alt=""></p>
<p>실제로 ViewModel의 viewModelScope를 확인해보면 내부적으로 SupervisorJob()과 Dispatchers.Main.immediate를 합친 CoroutineContext를 CoroutineScope에 넘겨주고 있는 것을 확인할 수 있습니다. 이건 아주 중요한 부분인데 viewModelScope가 supervisorScope라는 것입니다. 왜냐하면 ViewModel에서 하나의 코루틴이 실패하면 다른 코루틴들도 실패하고 취소되는 동작을 원치 않기 때문이고 이는 lifcycleScope의 내부 구현에서도 동일합니다.</p>
<p>직접 CoroutineScope를 구현하는 경우 이것을 이해하는 것이 매우 중요합니다.</p>
<h3 id="사람들이-자주하는-실수">사람들이 자주하는 실수</h3>
<hr>
<p>본문의 위에서 언급한 사람들이 코루틴에서 예외를 처리할 때 자주하는 실수에 대해 다뤄보고자 합니다.</p>
<pre><code>class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        lifecycleScope.launch {
            val job = launch {
                try {
                    delay(500L)
                } catch (e: Exception) {
                    e.printStackTrace()
                }
                println(&quot;Coroutine 1 finished&quot;)
            }
            delay(300L)
            job.cancel()
        }
    }
}</code></pre><p>위의 코드는 0.5초 뒤에 작업이 완료되는 job을 앱이 실행되고 0.3초 뒤에 취소하는 코드입니다. 일반적인 예상대로라면 job이 완료가 되기도 전에 취소했으므로 Coroutine 1은 완료되지 않아야하는데 Logcat에 CancellationException이 발생한 이후 &quot;Coroutine 1 finished&quot;가 출력된 것을 확인할 수 있습니다. 이러한 예상치 못한 동작이 발생한 이유를 알아봅시다.</p>
<p>위의 코드에서 job에 할당한 코루틴이 취소되면 어떤 일이 발생할까요?</p>
<p>여기서 suspend 함수인 delay()는 코루틴이 취소될 경우 CancellationException을 던집니다. 하지만 delay()는 일반적인 Exception을 처리하는 try-catch 블록 내에서 실행되고 있기에 CancellationException이 try-catch 블록에 의해 처리되어 버립니다. 해당 예외가 이미 처리되어 버렸기 때문에 제대로 전파되지 않으므로 외부의 CoroutineScope는 자식 Coroutine이 취소된 것을 알지 못하는 것이지요. 이것이 여전히 &quot;Coroutine 1 finished&quot;를 출력하는 이유입니다.</p>
<p>delay(), yield() 등과 같은 cancellable suspending function은 코루틴이 취소될 때 CancellationException을 던지는데, 추후 정리를 하여 링크를 연결시켜놓겠습니다.</p>
<p>그러면 이 문제를 어떻게 해결할 수 있을지 고민해봅시다.</p>
<hr>
<h4 id="방법-1">방법 1</h4>
<p>특정 예외를 정확히 catch하는 것으로 먼저 위의 문제를 해결할 수 있습니다.</p>
<p>이 코드를 실행하면 Logcat에 &quot;Coroutine 1 finished&quot;가 출력되지 않음을 확인할 수 있습니다.</p>
<pre><code>class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        lifecycleScope.launch { // 2. CancellationException이 부모 Scope까지 제대로 전파되어
            val job = launch {
                try {
                    delay(500L)
                } catch (e: HttpRetryException) {  // 1. CancellationException이 잡히지 않으므로
                    e.printStackTrace()
                }
                println(&quot;Coroutine 1 finished&quot;)  // 3. 이 라인의 작업을 실행하지 않는다.
            }
            delay(300L)
            job.cancel()
        }
    }
}
</code></pre><h4 id="방법-2">방법 2</h4>
<p>General Exception을 catch하고 싶을 경우, 해당 예외가 CancellationException일 경우 다시 예외를 던지는 코드를 작성하는 방법으로 해결할 수 있습니다.</p>
<p>마찬가지로 이 코드를 실행하면 Logcat에 &quot;Coroutine 1 finished&quot;가 출력되지 않음을 확인할 수 있습니다.</p>
<pre><code>class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        lifecycleScope.launch {  // 2. CancellationException이 부모 Scope까지 제대로 전파되도록 한다.
            val job = launch {
                try {
                    delay(500L)
                } catch (e: Exception) {
                    if (e is CancellationException) {
                        throw e  // 1. CancellationException일 경우 예외를 다시 던져
                    }
                    e.printStackTrace()
                }
                println(&quot;Coroutine 1 finished&quot;)
            }
            delay(300L)
            job.cancel()
        }
    }
}</code></pre><p>코루틴의 취소를 제대로 전파하지 않는 실수는 여러 사람의 코드에서 꽤나 발견되는 실수입니다. 하지만 이러한 실수로 인해 작성한 코루틴이 엉망이 될 수 있고, 이는 코루틴을 취소했는데도 여전히 작업을 수행하기 때문에 리소스를 낭비하는 결과를 초래합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] Coroutine 예외 처리 파헤치기]]></title>
            <link>https://velog.io/@kk_jang93/Android-Coroutine-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0</link>
            <guid>https://velog.io/@kk_jang93/Android-Coroutine-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0</guid>
            <pubDate>Fri, 12 Jul 2024 04:35:24 GMT</pubDate>
            <description><![CDATA[<p>코루틴을 막 학습한 사람에게 코루틴은 매우 간단하고 자바스크립트의 async, await와 비슷하게 보이기도 해서 비동기 프로그래밍을 위한 아주 쉽고 훌륭한 도구로 보일 수 있습니다. 실제로 쉽고 훌륭한 도구이긴 하지만요.</p>
<p>하지만 코루틴을 더 깊게 살펴보면 실제로 걸리기 쉬운 함정들이 많이 존재합니다. 예외 처리나 취소를 try-catch 블록을 통해 간단히 할 수 있으리라 생각하지만 실제로는 복잡한 매커니즘으로 동작하고 있기에 많은 것들이 잘못될 수도 있습니다.</p>
<ul>
<li>코루틴에서 어떻게 예외를 잡고 처리해야 하는지</li>
<li>코루틴에서 예외 처리가 일반적으로 어떻게 작동하는지</li>
<li>코루틴이 취소되거나 코루틴을 취소할 때 무엇을 고려해야 하는지</li>
</ul>
<pre><code>dependencies
implementation &#39;org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1&#39;
implementation &#39;androidx.lifecycle:lifecycle-runtime-ktx:2.5.0&#39;
implementation &#39;androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0&#39;</code></pre><hr>
<h2 id="많은-사람들이-오해하는-코루틴의-예외-처리">많은 사람들이 오해하는 코루틴의 예외 처리</h2>
<blockquote>
<h4 id="launch에서의-예외-처리">launch에서의 예외 처리</h4>
</blockquote>
<p>먼저 CoroutineScope인 lifecycleScope를 통해 코루틴 빌더인 launch를 수행하는 MainActivity 코드를 작성한 뒤, 내부에 예외를 던지는 자식 코루틴을 생성해봅시다.</p>
<pre><code>class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        lifecycleScope.launch { // 부모 코루틴
            launch { // 자식 코루틴
                throw Exception()
            }
        }
    }
}</code></pre><p>여기서 많은 사람들은 try-catch 블록을 통해 간단히 해당 예외를 처리할 수 있으리라 생각합니다. 실제로 아래의 코드를 실행하면 어떻게 될까요?</p>
<pre><code>class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        lifecycleScope.launch {
            try {
                launch {
                    throw Exception()
                }
            } catch (e: Exception) {
                println(&quot;Caught Exception: $e&quot;)
            }
        }
    }
}</code></pre><p><img src="https://velog.velcdn.com/images/kk_jang93/post/ebe9f3fe-341d-49c2-ba01-aeac1d1b378e/image.png" alt=""></p>
<ul>
<li><p><em><strong>try-catch</strong></em> 블록을 분명히 사용했는데도 크래시가 발생했네요. 
이것이 바로 코루틴에서 try-catch 블록이 제대로 동작하지 않는 경우입니다. 왜 이렇게 동작하는지 이해하기 위해서는 먼저 CoroutineScope와 코루틴이 동작하는 방식을 이해해야 합니다.</p>
</li>
<li><p>우리는 일반적으로 외부에 lifecycleScope, viewModelScope 또는 직접 생성한 Custom Scope와 같은 CoroutineScope를 가지고 이 Scope 내부에서 코루틴을 실행합니다.</p>
</li>
</ul>
<pre><code>lifecycleScope.launch {
    try {
        launch {
            throw Exception()
        }
    } catch (e: Exception) {
        println(&quot;Caught Exception: $e&quot;)
    }
}</code></pre><p>그리고 위의 코드처럼 해당 코루틴 내부에 자식 코루틴을 만들 수 있는데, 자식 코루틴 내부에서 예외를 던지면 어떤 일이 발생할까요?</p>
<p>참고로 해당 예외에 대한 가장 일반적인 예시는 Retrofit을 사용한 API 호출에서 HttpException이 발생하여 서버가 404 Not Found를 응답하는 경우 예외를 던지는 상황입니다.</p>
<p>순서는 다음과 같습니다.</p>
<pre><code>class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        lifecycleScope.launch {  // 4. 예외가 처리되지 않고 root 코루틴까지 예외가 전파됨(이 시점에 앱 크래시 발생)
            try {
                launch {  // 3. 여전히 예외가 처리되지 않았으므로 이 코루틴으로도 예외가 전파됨
                    launch {  // 2. 현재 코루틴으로 예외가 전파됨(propagation)
                        throw Exception()  // 1. 예외 발생
                    }
                }
            } catch (e: Exception) {
                println(&quot;Caught Exception: $e&quot;)
            }
        }
    }
}
</code></pre><p>코루틴에서 예외가 전파되는 것처럼 코루틴의 취소(cancellation)에서도 같은 일이 발생합니다.</p>
<p>코루틴이 취소될 때 CancellationException을 던지는데 이 예외는 항상 코루틴에서 처리되거나 잡히기 때문에 무언가 잘못되거나 나쁜 것이 아닙니다. 하지만 취소는 여전히 코루틴 트리로 전파되므로 부모 코루틴을 포함해 모든 자식 코루틴들이 특정 코루틴이 취소된 것을 감지합니다.</p>
<p>코루틴 트리는 structured concurrency의 동작 방식을 통해 코루틴이 내부적으로 트리 구조(부모-자식)의 형태로 관리가 되고 있음을 추측할 수 있는데 더 자세한 내용을 보고 싶으시다면 <strong><a href="https://suhwan.dev/2022/01/21/Kotlin-coroutine-structured-concurrency/">이 글</a></strong>을 참고하시면 좋을 것 같습니다.</p>
<blockquote>
<h4 id="async에서의-예외-처리">async에서의 예외 처리</h4>
</blockquote>
<p>lauch와 비교해서 asyc에서 예외 처리가 동작하는 방식의 차이점은 async는 await를 호출할 때 누적된 예외를 던지는 것입니다.</p>
<p>아래의 코드에서 await()는 root 코루틴인 launch 블록을 async 블록이 실행되고 0.5초 뒤에 Result 값을 사용가능 할 때까지 suspend됩니다.</p>
<pre><code>class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        lifecycleScope.launch {
            val string = async {
                delay(500L)
                &quot;Result&quot;
            }
            println(string.await())
        }
    }
}</code></pre><p>위에서 async는 await를 호출할 때 누적된 예외를 던진다고 했습니다. 그렇다면 아래의 코드는 어느 시점에 크래시를 발생시킬까요?</p>
<pre><code>lifecycleScope.launch {  // 2. 예외가 부모 코루틴으로 전파되어 앱에 크래시가 발생 
    val string = async {
        delay(500L)
        throw Exception(&quot;error&quot;)  // 1. 예외를 던지자마자
        &quot;Result&quot;
    }
    println(string.await())
}</code></pre><p>이 코드에선 await가 호출되는 시점에 예외를 던지지 않습니다. launch를 사용하고 있기 때문에 async 블록 내에서 예외를 던지자마자 앱에 크래시가 발생합니다.</p>
<p>그러면 개념을 다른 예시로 이해해보기 위해 이번엔 await() 라인을 삭제해보겠습니다.</p>
<pre><code>lifecycleScope.launch {
    val string = async {
        delay(500L)
        throw Exception(&quot;error&quot;) 
        &quot;Result&quot;
    }
}</code></pre><p>이 코드도 여전히 크래시가 발생합니다. 위에서 async는 await를 호출할 때 누적된 예외를 던진거나 전파한다고 했음에도 불구하고 왜 그러는걸까요?</p>
<p>코루틴에서 예외가 전파되는 매커니즘을 생각해봅시다. 위 코드에서 async 블록은 자식 코루틴이기 때문에 해당 블록 내에서 예외를 던지면 부모 코루틴인 launch 블록으로 예외를 전파시킵니다. 예외가 처리되지 않았다면 이전 launch 블록에서 예외를 던지는 코드들처럼 즉시 프로그램에 크래시를 발생시킵니다.</p>
<p>하지만 launch를 async로 대체하고 앱을 재실행해보면 크래시가 발생하지 않습니다. 자식 코루틴에서 발생한 예외가 부모 코루틴으로 전파되더라도 둘 다 async 블록이기 때문에 즉시 앱에 크래시를 발생시키지 않습니다.</p>
<pre><code>lifecycleScope.async {
    val string = async {
        delay(500L)
        throw Exception(&quot;error&quot;) 
        &quot;Result&quot;
    }
}</code></pre><p>외부 async 블록의 리턴값을 deferred에 담고 deferred를 다른 스코프 내부에서 deferred.await()를 통해 소비하도록 아래와 같이 코드를 작성하면 앱에 크래시가 발생합니다.</p>
<pre><code>val deferred = lifecycleScope.async {
    val string = async {
        delay(500L)
        &quot;Result&quot; 
    }
}
lifecycleScope.launch {  // 2. launch 블록은 앱에 크래시를 발생시킨다. 
    deferred.await()     // 1. 예외 처리를 별도로 하지 않았으므로 await()가 던진 예외가 위로 전파되고(raise) 
}                     
</code></pre><p>위와 같이 코드에 deferred가 있다면 아래의 코드처럼 await()를 호출하는 라인을 try-catch 블록으로 감싸주는 것으로 예외를 처리할 순 있습니다. 앱을 실행해도 크래시도 발생하지 않고요.</p>
<br>
<br>


<pre><code>val deferred = lifecycleScope.async {
    val string = async {
        delay(500L)
        &quot;Result&quot;
    }
}
lifecycleScope.launch {
    try {
        deferred.await()
    } catch (e: Exception) {
        e.printStackTrace()
    }
}</code></pre><p>하지만 이렇게 예외를 처리하면 잘못된 상황이 쉬우며 사람들이 많이 실수하는 코드이기도 합니다.</p>
<p>코루틴이 어떻게 동작하는지 더 알아본 뒤, 본문의 끝부분에서 위의 코드에서 발생한 실수에 대해 이야기하겠습니다.</p>
<br>


<blockquote>
<p>CoroutineExceptionHandler
그전에 try-catch 블록 이외에 예외를 처리하는 방법인 CoroutineExceptionHandler에 대해 알아보고자 합니다.</p>
</blockquote>
<pre><code>class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val handler = CoroutineExceptionHandler { _, throwable -&gt;
            println(&quot;Caught exception: $throwable&quot;)
        }

        lifecycleScope.launch(handler) {  // root 코루틴에 handler를 전달
            throw Exception(&quot;Error&quot;)
        }
    }
}</code></pre><p>CoroutineExceptionHandler를 생성한 뒤, 이 handler를 우리가 실행할 코루틴에 적용(install)시킬 수 있는데 반드시 <em><strong>root 코루틴</strong></em> 에게 적용시켜야 합니다.</p>
<p>CoroutineExceptionHandler는 root 코루틴의 모든 타입의 자식 코루틴에서 잡히지 않은 예외들을 처리할 수 있는 방법입니다.</p>
<p>주의해야할 점은 CoroutineExceptionHandler는 CancellationException을 잡지 않는다는 것입니다. 그렇기 때문에 코루틴 하나가 취소되더라도 CoroutineExceptionHandler의 블록은 실행되지 않습니다.</p>
<p>CancellationException과 코루틴은 코루틴이 취소되었다고 앱에서 크래시가 발생하는 것을 원하지 않을 것이기 때문에 기본적으로 처리가 되고, 앞에서 언급했듯 잡히지 않은 예외들만을 처리합니다.</p>
<p>내용이 많아 글이 길어지므로 다음 글 에서 이어서 진행하겠습니다.</p>
]]></description>
        </item>
    </channel>
</rss>