<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>312_dev.log</title>
        <link>https://velog.io/</link>
        <description>안드로이드 개발자 이상일입니다.</description>
        <lastBuildDate>Mon, 07 Oct 2024 07:49:48 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>312_dev.log</title>
            <url>https://velog.velcdn.com/images/312_log/profile/7a17ae6e-4c3d-418f-9adb-0c3390d8223c/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. 312_dev.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/312_log" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Flutter Web으로 앱 Testing 알아보기]]></title>
            <link>https://velog.io/@312_log/Flutter-Web%EC%9C%BC%EB%A1%9C-%EC%95%B1-Testing-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@312_log/Flutter-Web%EC%9C%BC%EB%A1%9C-%EC%95%B1-Testing-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Mon, 07 Oct 2024 07:49:48 GMT</pubDate>
            <description><![CDATA[<p>이번 포스팅은 Line ABCStudio의 김종식 님이 강연해주신 &quot;Flutter Web를 활용해 제품 개발 환경 개선하기&quot; 바탕으로 작성되었습니다.</p>
<p>최근 Future Flutter 행사에서 김종식님의 &quot;Flutter Web을 활용해 제품 개발 환경 개선하기&quot;라는 강연을 들을 기회가 있었다. 
이번 강연은 플러터 웹을 이용해 앱을 웹 환경에서 어떻게 구현하고, 발생하는 문제를 어떻게 해결했는지를 다룬 매우 실용적인 경험담이었다.</p>
<p><img src="https://velog.velcdn.com/images/312_log/post/3c13691a-2e70-4ef8-b10a-5d007b660ac1/image.png" alt=""></p>
<h2 id="focus-on-flutter-web">Focus on Flutter Web</h2>
<p><code>Flutter Web</code> 강연의 중점은 단순히 웹으로 UI를 그리는 것이 아니라, 웹 화면을 Flutter로 어떻게 효율적으로 보여주고 최적화할 수 있는지와 에러 핸들링에 있었다.
특히, Flutter로 개발한 모바일 앱을 웹에서 구현할 때 발생하는 여러 문제와 해결 과정을 다루며, 이를 통해 <code>Flutter Web</code>이 가지는 실제 사용성에 대한 시각을 제공해 주셨다.</p>
<h3 id="첫-번째-앱-웹-구현-초기-에러와-해결-방법">첫 번째 앱 웹 구현: 초기 에러와 해결 방법</h3>
<p>첫 번째 앱을 웹으로 변환하는 과정에서 가장 먼저 직면한 문제는 플랫폼 종속성을 언급하셨다. </p>
<p>Flutter는 다양한 플랫폼에서 동작하지만, <code>Platform.isAndroid</code>나 <code>Platform.isIOS</code>와 같은 플랫폼 체크 코드를 웹에서 사용하면 에러가 발생한다. 
이를 해결하기 위해서는 <code>kIsWeb</code>을 사용해 웹인지 여부를 정확히 분기 처리해야 했고,
이를 통해 불필요한 플랫폼 구분 코드를 제거하고, 플랫폼 간 호환성을 유지할 수 있었다고 하셨다.</p>
<p>또 다른 중요한 이슈로는 <code>Flutter Secure Storage</code>를 사용할 때 발생한 <code>DomException</code>다. 
이 오류는 웹 페이지 세션이 종료된 후에도 데이터가 브라우저의 데이터베이스(DB)에 저장되면서 발생했고, 보안이 취약할 수 있는 웹 환경에서 데이터가 저장되는 것은 문제가 될 수 있었다. 
이 문제를 해결하기 위해서는 데이터 저장 방식에 대한 근본적인 수정이 필요했다.</p>
<p>web 개발을 하면서 한번쯤 보게되는 <code>CORS</code> 이슈도 큰 문제 중 하나였다. 
웹에서 다른 출처의 리소스를 요청할 때 발생하는 <code>XMLHttpRequest error</code>가 원인이었고, 해당 요청이 보안 정책에 의해 차단되었기 때문에 발생했다. 
로컬 환경에서는 <code>disable-web-security</code> 옵션을 사용해 우회할 수 있었지만 이는 배포 환경에서는 절대 사용할 수 없는 임시 해결책임을 강조하셨다.</p>
<h3 id="두-번째-웹-프로젝트-관계자와-공유하기-위한-웹-버전">두 번째 웹 프로젝트: 관계자와 공유하기 위한 웹 버전</h3>
<p>두 번째 프로젝트에서는 이미 웹으로 제공되는 앱을, 관계자들에게 새로운 기능을 보여주기 위해 사용했다고 하셨다.</p>
<p>이 과정에서 모바일과 동일한 UI/UX를 웹에서 구현하는 것이 핵심 과제였다.
<strong>커스텀 린트(Custom Lint)</strong> 를 사용해 플랫폼 구분과 에러 처리에 대한 코드 분기를 자동화하고, 에러에 효율적으로 대응할 수 있었다.</p>
<p>시간이 지남에 따라 <code>Flutter</code>의 패키지들이 웹 지원을 시작하면서, 첫 번째 프로젝트 때보다 훨씬 수월하게 대응할 수 있었다고 하셨다. 
그러나 모든 기능을 웹에서 제공할 수 없기 때문에, 의도적으로 웹에서 특정 기능을 제한하는 방식도 필요했고, <code>CustomLint</code>를 통해 웹에 맞는 구현체를 반환하도록 코드를 수정하셨다.</p>
<p>특히 <code>Google Maps JavaScript API</code>를 활용해 지도 기능을 구현한 경험을 공유해 주셨는데, 배달 앱을 내부 웹으로 구현했을 때 직원들이 오히려 웹 버전으로 배달을 시키는 일이 발생했다고 하셨다. 
웹으로 주문하는 것이 컴퓨터를 사용하는 환경에서는 오히려 편했기에 현장 결제 기능을 활용하는 유저에게 더 쉽게 다가갈 수 있는 흥미로운 이야기였다.</p>
<h3 id="세-번째-프로젝트-리테일-앱의-웹-배포">세 번째 프로젝트: 리테일 앱의 웹 배포</h3>
<p>세 번째 프로젝트에서는 리테일 앱을 웹으로 별도로 배포하는 과정에서 그 &#39;<code>CORS</code>&#39; 이슈가 매우 많이 발생했다고 하셨다. </p>
<p>이 문제를 해결하기 위해 서버 개발자 및 인프라 개발자들과 긴밀히 협력해 다른 출처의 리소스 문제를 해결했다고 하셨지만 정말 수많은 개발단계에서의 고충이 있었을 것 같았다... </p>
<p>이 과정에서 웹 환경에서 발생하는 이슈를 미리 예측하고, 개발 단계에서부터 서버와의 협력을 매우 중요하게 언급하셨다.</p>
<h3 id="웹-환경에서의-팁과-교훈">웹 환경에서의 팁과 교훈</h3>
<p>강연을 마무리하시면서 몇가지 팁을 제공해주셨다. 
먼저, <code>Platform.isAndroid</code>나 <code>Platform.isIOS</code>를 사용하는 것을 지양하고, 웹 오류를 확인하고 분기 처리하는 것이 중요하다는 점을 강조하셨다.</p>
<p>또한, 특정 기능이 웹 환경에서 반드시 필요한지, 혹은 불필요하게 웹에서 제공될 필요가 없는지에 대한 고민이 필요하다고 하셨다.</p>
<p>브라우저 쿠키 및 캐시 제거와 관련된 제약사항도 중요한 이슈로 언급되었다. 
쿠키 대응에 대해서는, 실제 서비스 환경이 아니었기 때문에 디테일한 대응을 하진 못했지만, 함께 작업하는 사용자들 덕분에 문제를 비교적 수월하게 해결할 수 있었다고 하셨다.
(쿠키 및 캐시 대응은 웹 개발에서 가장 어려운 것 같다..)</p>
<h3 id="question">Question</h3>
<p>질문으로 &quot;테스트 디바이스를 활용하는 것이 더 낫지 않은가?&quot; 라는 질문이 들어왔었다. </p>
<p>테스트 디바이스 제공이 중요한 경우도 있지만, 인원 수가 많고 물리적인 거리가 있는 경우 웹으로 바로 기능을 공유하는 것이 훨씬 더 효율적이라는 답변을 남겨주셨고, 개인적으로 테스트 디바이스는 QA 같은 실제와 같아야 하는 환경에서는 유효하지만 빠르게 개발하고 컨펌을 받아야하는 기획자나 디자이너와는 <code>Flutter Web</code>이 굉장히 유효하다고 생각했다.</p>
<h3 id="future--flutter-web">Future &lt; Flutter Web &gt;</h3>
<p>강연은 단순히 에러 해결 과정을 나열하는 것 말고도 지금 현재의 <code>Flutter Web</code>과 미래의 환경에 대해 생각해 볼 수 있었다. </p>
<p>과거의 <code>Flutter Web</code>이 가진 불편한 제약에도 불구하고, 이를 극복하고 실제 제품 개발 환경에서 어떻게 활용할 수 있는지와 현재에도 <code>Flutter Web</code>이 어떻게 활용 될 수 있는지 보여주셨다.</p>
<p>그러나 내 생각에는 여전히 Flutter Web은 다른 웹 기술에 비해 &#39;앱을 그대로 웹에서 보여주는&#39; 수준에 머물러 있다는 점에서 아쉬움이 남았다. </p>
<p>내년에는 전폭적으로 지원된다는 <code>Flutter Web</code>이 더욱 발전하여 웹 환경에서 더욱 경쟁력 있는 솔루션이 되길 기대해본다!</p>
<h3 id="references">References</h3>
<p>출처 : * Flutter Web를 활용해 제품 개발 환경 개선하기 - 김종식@Line ABCStudio
Future Flutter에서의 강연을 기반으로 작성되었으며, 문제 시 삭제하겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Widget으로 Flutter 렌더링 구조 알아보기]]></title>
            <link>https://velog.io/@312_log/Widget%EC%9C%BC%EB%A1%9C-Flutter-%EB%A0%8C%EB%8D%94%EB%A7%81-%EA%B5%AC%EC%A1%B0-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@312_log/Widget%EC%9C%BC%EB%A1%9C-Flutter-%EB%A0%8C%EB%8D%94%EB%A7%81-%EA%B5%AC%EC%A1%B0-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Wed, 02 Oct 2024 05:14:25 GMT</pubDate>
            <description><![CDATA[<p>이번 포스팅에서는 Future Flutter에서 에이든님의 강연을 바탕으로 Flutter의 렌더링 엔진의 내부 구조를 알아보려고 한다.</p>
<p><img src="https://velog.velcdn.com/images/312_log/post/b50a8020-cc41-432f-820b-3ae2cb415e9d/image.png" alt="">
[HotReload를 통한 위젯의 부분 렌더링을 퍼즐로 표현해봤다]</p>
<p>Flutter가 부드럽고 효율적인 UI를 제공할 수 있는 이유는 렌더링 파이프라인 덕분이다.</p>
<h2 id="모든-것은-위젯에서-시작된다">모든 것은 위젯에서 시작된다</h2>
<p>플러터에서 모든 UI는 <code>widget</code>으로 시작하고 끝난다. 
버튼에서 컨테이너에 이르기까지, 모든 시각적 요소가 <code>widget</code>이다. 
이러한 구조 덕분에 우리는 <code>widget</code>을 중첩하고 조합하여 UI를 구성하게 된다.</p>
<p>우리가 앱을 구동하면 시작되는 <code>runApp</code> 함수부터 <code>widget</code>을 호출하고 앱이 종료될때까지 <code>widget</code>을 보게 된다.</p>
<p>플러터에서는 <code>widget</code>을 모든 시각적 요소를 나타낸다고 표현하고 있다.</p>
<h3 id="runapp의-역할">runApp의 역할</h3>
<p><code>runApp</code> 함수는 모든 Flutter 앱의 시작점이다. 
이 함수는 위젯 트리를 생성하고 이를 화면에 연결하는 역할을 한다. 
<code>runApp</code>이 호출되면, 내부적으로 <code>_runWidget</code> 함수가 실행되고, 
이는 다시 <code>attachRootWidget</code>을 호출하여 루트 위젯을 빌드 소유자에 연결한다. 
이 과정을 통해 Flutter의 렌더링 사이클이 시작된다.</p>
<h3 id="rendering--from-위젯-to-캔버스">(Rendering)-&gt; from 위젯 to 캔버스</h3>
<p>플러터에서 렌더링은 <code>widget</code>과 <code>RenderObject</code>의 계층을 통해 이루어진다. 
<code>widget</code>은 UI의 모양을 정의하고, <code>RenderObject</code>는 실제로 이를 화면에 그린다. 
최종적으로 이 모든 과정은 캔버스에 그려지며, 사용자가 볼 수 있는 화면이 완성된다.</p>
<h3 id="렌더링-과정-한눈에-보기">렌더링 과정 한눈에 보기</h3>
<p><code>runApp</code> → <code>_runWidget</code> → <code>attachRootWidget</code>: 위젯 트리 시작
<code>Widget</code> → <code>Element</code> → <code>RenderObject</code>: 계층 구조 생성
<code>RenderObject</code> → <code>Canvas</code>: 최종 렌더링 및 그리기
<code>Canvas</code> → 화면: 사용자에게 표시</p>
<p>이러한 과정은 각 단계가 유기적으로 연결되어 작동하며, Flutter가 효율적인 UI 렌더링을 가능하게 한다.</p>
<h2 id="coloredbox의-확장-과정과-이유">ColoredBox의 확장 과정과 이유</h2>
<p>강연에서는 간단한 위젯인 <code>ColoredBox</code>를 통해 렌더링 예시를 보여주셨다.
간단해 보이는 <code>ColoredBox</code> 위젯도 실제로는 깊은 계층 구조를 갖는다. 
<code>ColoredBox</code>는 <code>SingleChildRenderObjectWidget</code>을 확장하고, 이는 다시 <code>RenderObjectWidget</code>을 확장하며, 궁극적으로는 Widget 클래스를 상속받는다. 
이처럼 플러터는 가장 기본적인 위젯조차 복잡한 시스템을 기반으로 한다.</p>
<p>왜 이렇게 복잡한 구조로 <code>ColoredBox</code>가 구현되었을까?</p>
<h3 id="유연한-ui의-구성">유연한 UI의 구성</h3>
<p>플러터는 복잡한 UI의 구현을 위해 기본 위젯을 가지고 다양한 용도에 쓸 수 있도록 확장할 수 있다. <code>ColoredBox</code>같은 간단한 위젯도 여러 클래스 계층을 통해 다양한 기능을 지원하고 커스터마이징 할 수 있게하기 때문이다.</p>
<p>사실 이러한 부분은 대부분의 최신 UI에서 채택하는 구조이기도 하다.</p>
<h3 id="ui-성능-최적화">UI 성능 최적화</h3>
<p>에이든의 강연에서 중요한 포인트 중 하나는 Flutter 렌더링을 최적화하는 방법이었다. 
불필요한 위젯 재구성을 피하고, 위젯의 크기, 색상 등의 파라미터를 효율적으로 관리함으로써 성능을 크게 향상시킬 수 있다. </p>
<p>예를 들어, 커스텀 위젯이 불필요한 전체 핫 리로드를 요청하지 않도록 하면 UI 스레드의 작업 부하를 줄일 수 있다.</p>
<p>그러면 왜 계층을 통해 유연하고 효율적으로 UI를 구성해야 할까?
그리고 어느정도 수준의 목적을 만족시켜야 하는지 알아보자.</p>
<h2 id="효율적인-ui의-목적">효율적인 UI의 목적</h2>
<h3 id="element와-buildcontext의-역할">Element와 BuildContext의 역할</h3>
<p>Flutter에서 <code>Element</code>는 UI 트리를 구성하고 <code>BuildContext</code>를 호출하는 요소이다. 
각 <code>widget</code>은 생성될 때 <code>Element</code>를 갖고 <code>Element</code>는 <code>BuildContext</code>와 연결되며, 
이는 <code>widget</code>의 위치를 관리하는 역할을 한다.
(<code>Element</code>가 <code>widget</code>보다 <code>lifecycle</code>이 길다)
이러한 구조는 Flutter가 UI를 효율적으로 빌드하고 업데이트하는 데 중요한 기초가 된다.</p>
<p>이 <code>Element</code>의 업데이트 주기와 횟수를 통해 우리는 UI를 보고 자연스럽게 움직이거나 이동하는것을 느끼게 된다.</p>
<h3 id="프레임-속도-유지">프레임 속도 유지</h3>
<p>Flutter는 <code>Fragment Shader</code>를 통해 GPU의 성능을 활용하여 렌더링 작업을 처리한다. 
이 셰이더는 수백만 개의 픽셀을 병렬로 처리할 수 있어 고해상도 이미지나 복잡한 그래픽을 효율적으로 다룰 수 있다.</p>
<p>이를 이용해 부드러운 애니메이션과 상호작용을 보장하려면 플러터는 <code>build</code>와 <code>paint</code> 단계가 각각 16ms 내에 처리가 이루어져야 한다.
초당 (<code>build</code>&lt;= 16fps) + (<code>paint</code>&lt;= 16fps) =&gt; 30fps를 유지할 수 있다.</p>
<p>최신 플래그십 기기들은 60fps, 120fps를 지원할 정도로 하드웨어 성능이 좋아졌기 때문에 효율적이고 빠른 빌드와 페인트 수준을 유지하는 것이 중요하다.</p>
<p>성능이 제한된 염가형 기기나 보급형 장치에서도 이 프레임 속도를 유지하려면 빌드 프로세스를 최적화하고 페인트 시간을 줄이는 것이 핵심이다.</p>
<h2 id="렌더링-최적화의-결론">렌더링 최적화의 결론</h2>
<p>Flutter 렌더링 최적화의 핵심은 &quot;불필요한 그리기를 하지 않는 것&quot;이었다. 
재구성되는 위젯을 최소화하고, 최적화된 커스텀 위젯을 사용하는 것이 앱을 가볍고 효율적으로 만드는 비결이다. 
상태 관리도 중요한 역할을 하며, 최대한 UI 상태가 변경될 때만 업데이트가 호출되도록 해야 한다고 언급하셨다.</p>
<h3 id="references">References</h3>
<p>출처 : 에이든님의 플러터 렌더링 해부학
Future Flutter에서의 강연을 기반으로 작성되었으며, 문제 시 삭제하겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[FFI로 알아보는 Flutter에서 GO Lang 사용하기]]></title>
            <link>https://velog.io/@312_log/FFI%EB%A1%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EB%8A%94-Flutter%EC%97%90%EC%84%9C-GO-Lang-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@312_log/FFI%EB%A1%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EB%8A%94-Flutter%EC%97%90%EC%84%9C-GO-Lang-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 30 Sep 2024 06:01:26 GMT</pubDate>
            <description><![CDATA[<p>최근 Future Flutter 행사에 다녀오면서 플러터를 다양한 환경에서 사용하고 있는 분들을 많이 만나게 되었다. 이번 주에는 이와 관련된 지식들을 포스팅을 통해 정리해보려 한다.</p>
<p>첫 강연은 Unity의 리핵님이 진행하신, 플러터에서 <code>Go</code> 언어를 사용하는 방법에 대한 내용이었다. 가장 먼저 떠오른 생각은 &#39;왜 <code>Dart</code>가 아닌 <code>Go</code>를 사용해야 하는가?&#39;였다.</p>
<p><img src="https://velog.velcdn.com/images/312_log/post/942c7317-248c-4fae-98cc-9ac5c79165b9/image.png" alt=""></p>
<h3 id="why-go-lang">Why Go Lang?</h3>
<p><code>Go</code>는 <code>C</code> 언어를 기반으로 한 비교적 저수준(Low-level) 언어다.
저수준 언어란 사람이 작성하는 코드가 컴퓨터가 이해할 수 있는 기계어로 변환되는 과정이 짧다는 것을 의미한다. </p>
<p>저수준 언어는 프로그래머가 코드를 작성하기 어렵지만 빌드와 런타임 속도가 빠르다.
 반면, 고수준 언어는 작성하기 쉽지만 속도가 상대적으로 느리다는 단점이 있다.</p>
<p><code>C</code>를 기반으로 한 <code>C++</code>, <code>C#</code>, <code>Go</code> 같은 언어들은 <code>Dart</code>보다 상대적으로 빠른 속도로 복잡한 계산을 효율적으로 처리할 수 있기 때문에, 강연자님도 <code>Go</code>를 선택했다고 하셨다.</p>
<h3 id="why-flutter">Why Flutter?</h3>
<p>사실 다른 언어를 사용하는 방법은 네이티브 환경에서도 충분히 가능하다. 
그럼에도 불구하고 플러터를 선택한 이유는 <code>크로스 플랫폼</code>의 이점과 <code>스키아 엔진</code>의 효율적인 렌더링 때문이라고 언급하셨다. </p>
<p>비즈니스 로직과 연산은 <code>Go</code>로 처리하고 UI는 보여주기만 하면 되는 <code>Go</code> 모듈 환경에서 <code>크로스 플랫폼</code>을 지원하는 플러터는 리소스의 낭비가 없는 적합한 선택이었고,
<code>스키아 엔진</code>의 효율적인 렌더링 덕분에 변경된 부분만 빠르게 업데이트할 수 있는 <code>Hot reload</code>가 가능했다.</p>
<h3 id="how-to-use">How to use?</h3>
<p><code>FFI</code>(Foreign Function Interface) 플러그인을 활용하여 <code>Go</code> 모듈을 가져올 수 있다. 기본적으로 <code>lib</code> 안의 <code>main</code> 함수에서 <code>src</code> 안의 <code>Go</code> 모듈을 호출해 결과를 리턴받고, 이를 UI에 적용하는 방식이다.</p>
<p>기본적으로 지원되지 않기 때문에 안드로이드와 iOS용으로 각각 크로스 컴파일을 해줘야 하며, <code>FFIgen</code> 파일을 통해 함수별 바인딩도 설정해야 한다. </p>
<p>자세한 방법은 해당 링크를 참고하면 된다.</p>
<p><a href="https://dev.to/leehack/how-to-use-golang-in-flutter-application-golang-ffi-1950">https://dev.to/leehack/how-to-use-golang-in-flutter-application-golang-ffi-1950</a></p>
<h3 id="when-to-use">When to use?</h3>
<p>그러면 <code>Go</code> 모듈을 활용한 고성능, 고효율 앱은 어디에 사용할까? 
한 참가자가 이에 대해 질문을 했고, 강연자님은 대표적으로 머신러닝을 언급했다.
머신러닝이 일반적인 환경에서 자주 사용되지는 않지만, 플러터에서 이런 방식으로 활용하는 것도 가능하다는 것을 보여준 강연이었다.</p>
<p>또한, 유니티 개발자로 근무하시는 분이라 게임 환경에서의 최적화도 고려하셨을수도 있을 것 같았다.</p>
<h3 id="references">References</h3>
<p>출처 : 리핵@Unity 님의 FFI로 연결하는 고(Go)와 플러터
Future Flutter에서의 강연을 기반으로 작성되었으며, 문제 시 삭제하겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Google Login Chain validation failed 에러 해결]]></title>
            <link>https://velog.io/@312_log/Google-Login-Chain-validation-failed-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@312_log/Google-Login-Chain-validation-failed-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Tue, 25 Jun 2024 11:35:22 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/312_log/post/2697b86b-770e-4403-b0dd-3fd5f3a97d74/image.png" alt="">
사이드 프로젝트 앱을 개발하던 중에 해당 에러가 발생하면서 로그인이 되지 않았다.</p>
<pre><code>  &lt;-- HTTP FAILED: javax.net.ssl.SSLHandshakeException: Chain validation failed</code></pre><p>로그에서도 동일한 에러를 발생시키며 로그인이 되지 않았다.</p>
<p>이전에 <code>GoogleLogin</code> 을 통합적으로 관리하던 <code>Firebase Console</code> 에서 <code>SHA-1</code> 키가 <code>debug</code>, <code>release</code>, <code>playstore</code> 버전 모두 다른 키를 등록해주지 않았을 때, 등록되지 않은 버전에서 로그인 이슈가 있었다.</p>
<p>그런데 해당 console API를 점검해봤지만 문제가 없었고, https로 접속되던 OAuth 로그인 페이지를 http로 비보안 로그인 해보니 <code>token</code>을 정상적으로 반환했다.</p>
<p><img src="https://velog.velcdn.com/images/312_log/post/4766bfc3-e78b-4622-b922-a294a996fc00/image.png" alt=""></p>
<p>백엔드 개발자님께 해당 이슈를 인계했고, aws 서버를 사용하던 앱의 서버에 수동으로 인증서를 붙이고 자동으로 갱신되게끔 설정해뒀는데, ssl인증서가 만료되었고 자동으로 갱신되지 않았기 때문이라는 이유를 들을 수 있었다..!</p>
<p>해당 에러는 안드로이드 환경에서 더 해결할 방법은 없다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Android의 StateFlow, SharedFlow를 알아보자]]></title>
            <link>https://velog.io/@312_log/android-viewmodel</link>
            <guid>https://velog.io/@312_log/android-viewmodel</guid>
            <pubDate>Thu, 30 May 2024 16:58:29 GMT</pubDate>
            <description><![CDATA[<h2 id="what-is-stateflow">What is StateFlow?</h2>
<p><img src="https://velog.velcdn.com/images/312_log/post/c9b3ebcc-6150-4253-be84-cbb862d1c698/image.png" alt=""></p>
<p>(<code>StateFlow</code>와 <code>SharedFlow</code>는 흐름에서 최적으로 상태 업데이트를 내보내고 여러 소비자에게 값을 내보낼 수 있는 <code>Flow API</code>이다.)</p>
<p><code>StateFlow</code>가 사용되기 이전에는 <code>LiveData</code> 클래스를 사용해서 데이터 갱신을 수집했었다.</p>
<p><code>LiveData</code>는 <code>Observable</code>한 데이터 홀더 클래스이기 때문에 Observe 하는 클래스에서 데이터의 변경을 감지하고 상태를 업데이트하는데 사용되었다.</p>
<p>같은 역할을 <code>StateFlow</code>도 수행하는데 왜 <code>StateFlow</code>가 등장했는지 알아보자.</p>
<h2 id="why-not-livedata">Why Not LiveData?</h2>
<ul>
<li><p><code>LiveData</code> 는 AAC 라이브러리이기 때문에 의존성을 갖지 않아야 하는 클린 아키텍처의 Domain Layer에서는 사용할 수 없다.</p>
</li>
<li><p><code>LiveData</code>은 <code>Main Thread</code>에서만 동작할 수 있기 때문에 비동기 처리에 한계가 있다.</p>
<p><code>MediatorLiveData</code>, <code>Transformations</code>, <code>Coroutines</code>와 같은 추가적인 도구를 사용해야 한다.</p>
</li>
</ul>
<p>이러한 단점은 <code>StateFlow</code> 의 등장 배경이 되었다..!</p>
<h2 id="whats-new-in-stateflow-sharedflow">What&#39;s new in StateFlow, SharedFlow?</h2>
<p>Flow가 등장함에 따라서 <code>LiveData</code>의 대체 가능성이 생겼고,</p>
<p><code>StateFlow</code>, <code>SharedFlow</code>이 함께 등장해서 <code>LiveData</code>를 완전히 대체 할 수 있게 되었다.</p>
<p><code>StateFlow</code>와 <code>SharedFlow</code> 둘 다 HotStream을 기반으로 Collect() 이후부터 데이터를 가져올 수 있다.<br><code>StateFlow</code>는</p>
<ul>
<li>초기값이 존재한다. </li>
<li>value속성을 이용해 값을 방출한다.</li>
<li>flow를 공유할 수 있으며 항상 최신 값(마지막 데이터 값)을 받는다.</li>
<li>내부적으로 값이 같을 경우 갱신하지 않는다.</li>
<li>효율적이고 API가 더 간단하므로 상태관리에 잘 어울린다.</li>
</ul>
<p><code>SharedFlow</code>는 </p>
<ul>
<li>초기값이 없어도 된다. </li>
<li>replayCache를 통해 collect시 전달받을 이전 데이터의 개수와 캐싱 여부까지 정의할 수 있다.</li>
<li>시간에 따라 동일한 이벤트를 트리거하는 경우에 더 적합하게 동일한 값을 재방출한다.</li>
</ul>
<h2 id="how-to-stateflow">How To StateFlow?</h2>
<p><img src="https://velog.velcdn.com/images/312_log/post/b3e6b5dc-546e-4591-a2a4-870aacd7461c/image.png" alt=""></p>
<p>로딩화면에 적용한 예시이다.</p>
<p><code>_isLoading</code>을 변경가능하게 <code>private</code>로 선언해서 외부에서 접근할 수 없게 만들고 <code>isLoading</code>으로 데이터를 발행해서 변경되는 값을 최신으로 구독할 수 있게 만들었다.</p>
<h2 id="how-to-sharedflow">How To SharedFlow?</h2>
<p><img src="https://velog.velcdn.com/images/312_log/post/335cef0b-60b0-4cab-8684-6fc16c09240e/image.png" alt=""></p>
<p>로딩화면을 <code>SharedFlow</code>로 다시 만들어봤다.</p>
<ul>
<li><code>replay</code>는 새로운 구독자들에게 이전 이벤트의 방출 여부 (0 미방출 1 방출)</li>
<li><code>extraBufferCapacity</code>는 추가 buffer 생성 여부 (1  생성)</li>
<li><code>OnBufferOverflow</code>는 버퍼 초과시 처리 여부 (DROP_OLDEST = 가장 오래된 데이터 drop)</li>
</ul>
<h3 id="결론">결론</h3>
<p><code>StateFlow</code>는 <code>LiveData</code>의 대체 데이터 홀더 클래스
<code>SharedFlow</code>는 Flow의 기능을 활용한 디테일한 데이터 홀더 클래스</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[UseCase를 알아보자]]></title>
            <link>https://velog.io/@312_log/UseCase%EB%A5%BC-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@312_log/UseCase%EB%A5%BC-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Wed, 29 May 2024 14:03:04 GMT</pubDate>
            <description><![CDATA[<h2 id="what-is-usecase-">What is UseCase ?</h2>
<p>UseCase 패턴은 <strong>사용자</strong> 관점에서 시스템의 서비스 기능 관계를 다이어그램으로 표현한 것이다.</p>
<p>즉 사용자가 서비스에서 수행하고자 하는 기능들을 UseCase라고 한다.</p>
<p>UseCase 패턴에서는 기능의 UseCase를 선언해두고 사용하는 로직이나 호출하는 로직에서 해당 UseCase를 통해 소통하는 것을 원칙으로 한다.</p>
<h2 id="why-usecase">Why UseCase?</h2>
<p><img src="https://velog.velcdn.com/images/312_log/post/8fbdb977-ff48-4a9b-a1df-9c49a9442b4c/image.png" alt=""></p>
<p>UseCase를 자판기로 표현해보자. </p>
<p><code>돈을 넣는다</code> 
<code>원하는 음료 버튼을 누르면 음료수가 나온다</code></p>
<p>UseCase 패턴을 적용하지 않는다면 음료수를 마시는 과정이 하나의 기능처럼 작동해서 돈이나 음료의 종류나 액수 변경 등의 기능 확장과 유지보수가 어렵게 작동할 수 있다.</p>
<p>기능을 분리하더라도 서비스가 확장될수록 기능의 흐름이 한눈에 파악되지 않고 어디까지 기능을 분리하고 통합할지 고민하게 된다.</p>
<p>이러한 문제때문에 사용자 관점에서 기능을 분리해 기능의 흐름대로 UseCase를 호출하고 그 UseCase 아래에서 각각의 기능을 실행하는 UseCase 패턴을 구축한다.</p>
<p>예를들어, &quot;지폐를 추가한다&quot; 는 기능을 추가할 때 &quot;돈을 넣는다&quot; UseCase의 하위 로직만 수정하고 테스트하면 다른 코드의 신뢰성을 유지할 수 있다.</p>
<p>또한, 중간에 &quot;음료의 온도를 유지한다&quot; UseCase가 추가된다고 해도 코드와 코드의 중간이 아닌 UseCase의 사이에 넣는다거나 순서가 바뀌어도 유동적으로 대처할 수 있게 된다.</p>
<h2 id="how-to-usecase">How To UseCase?</h2>
<p>Kotlin으로 작성된 로그인 코드를 살펴보자.</p>
<pre><code>
class FetchAccessTokenUseCase @Inject constructor(
    private val loginRepository: LoginRepository
) {
    suspend operator fun invoke(provider: Provider, token: String) {
        loginRepository.fetchAccessToken(provider, token)
    }
}

class DeleteTokenUseCase @Inject constructor(
    private val loginRepository: LoginRepository
) {
    suspend operator fun invoke() {
        loginRepository.deleteToken()
    }
}</code></pre><p><code>토큰을 통해 로그인 하는 기능</code>, <code>토큰을 삭제하고 로그아웃 하는 기능</code>의 두가지 기능이 있다.</p>
<p>실제로 로그인/아웃 기능을 사용하는 로그인 페이지에서는 해당 UseCase를 호출하고 UseCase는 <code>LoginRepository</code> 를 통해 토큰 저장소와 소통한다.</p>
<p>이렇게 하면 로그인 페이지에서 직접적으로 Repository를 선언해주지 않아도 되고 기능의 변경이나 확장이 있을때도, UseCase를 추가하고 기능을 구현해주면 된다.</p>
<p>UseCase 패턴을 통해 가독성 좋고 유지보수성 효율성 높은 코드를 만들수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Android의 MVVM을 알아보자 with AAC ViewModel]]></title>
            <link>https://velog.io/@312_log/android-In-MVVM</link>
            <guid>https://velog.io/@312_log/android-In-MVVM</guid>
            <pubDate>Tue, 28 May 2024 15:02:00 GMT</pubDate>
            <description><![CDATA[<h2 id="andorid에서-mvvm이란">Andorid에서 MVVM이란?</h2>
<p>MVVM 디자인 패턴은 Model-View-ViewModel 이라는 세가지 구성요소를 통해 비즈니스 로직과 View를 구분한다. </p>
<p>마찬가지로 Android 환경에서 같은 이름의 <strong>AAC ViewModel이 MVVM ViewModel의 역할을 한다고 생각했지만 이름만 같을 뿐 실제로는 다른 역할을 수행</strong>하게 된다.</p>
<p>흔히 Android 개발 환경에서 MVVM패턴을 많이 사용해서 오용하는 경우가 있지만, 공식문서의 디자인 패턴에서 MVVM패턴은 언급되지 않는다.</p>
<h3 id="what-is-viewmodel-in-android-aac-viewmodel">What is ViewModel in Android? (AAC ViewModel)</h3>
<p><img src="https://velog.velcdn.com/images/312_log/post/ff1d3fcc-1103-4a60-81e4-3ed369e125be/image.png" alt=""></p>
<p>Developers에서 ViewModel 항목을 읽어보면 비즈니스 로직 또는 화면 수준 상태 홀더(screen level state holder) 의 역할을 수행한다.</p>
<p><strong>Android에서의 ViewModel은 상태 변화가 일어나는 환경에서 UI 관련 데이터를 Lifecycle 내에서 관리하고 유지하는 클래스</strong>이다. </p>
<p>해석하면 AAC ViewModel은 데이터의 관리와 Binding의 목적보다는 Activity나 Fragment을 다시 그려야 할 경우 데이터를 다시 로드시키거나 이로인해 발생하는 사이드이펙트의 최소화가 주 목적인 클래스다.</p>
<h3 id="how-about-mvvm-viewmodel">How About MVVM ViewModel?</h3>
<p><strong>MVVM에서의 ViewModel은 View와 Model을 이어주는 Binding이 주 목적인 클래스</strong>다.
값의 변경이 발생하면 그 상태를 View에 알리고 View는 그 변경을 반영하게 된다.</p>
<p>흔히 사용하는 <code>MutableLiveData</code> 나 <code>MutableStateFlow</code>와 <code>Observer</code>등이 이 역할을 하며 View는 Activity나 Fragment가 아닌 <code>@Composable</code> 이나 <code>XML</code>같은 View에 직접 전달해야 MVVM을 제대로 구현했다고 할 수 있다.</p>
<p>Activity나 Fragment에서 ViewModel을 통해 observer패턴을 구현하면 View가 ViewModel이 아닌 Activity나 Fragment를 의존하기 때문에 잘못 구현된 MVVM의 사례라고 한다.</p>
<h3 id="mvvm-viewmodel-with-aac-viewmodel">MVVM ViewModel With AAC ViewModel</h3>
<p>그러나 Android 환경에서 가장 많이 사용하는 패턴이 MVVM인 만큼 MVVM은 AAC ViewModel과 함께 구현할 수 있다.</p>
<p>주 목적은 아니지만 AAC ViewModel에서 State의 관리를 전담하고 binding할 요소를 추가해주면 일반적으로 사용하는 MVVM ViewModel처럼 사용하게 된다.</p>
<p>하지만 AAC ViewModel을 사용했다고 MVVM 패턴을 차용한 것은 아니므로 디자인 패턴과 의존성에 유의해야 한다.</p>
<p>추가로 공식 문서에서는 Activity와 ViewModel을 1대1로 두고 사용하는 방법을 권장한다.</p>
<h3 id="refercences">Refercences</h3>
<p><a href="https://velog.io/@spdlqjfire/Android-AAC-ViewModel-vs.-MVVM-ViewModel">Android - AAC ViewModel vs. MVVM ViewModel</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Given - When - Then]]></title>
            <link>https://velog.io/@312_log/given-when-then</link>
            <guid>https://velog.io/@312_log/given-when-then</guid>
            <pubDate>Mon, 27 May 2024 12:27:00 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/312_log/post/407d3bf3-b08a-4351-a99b-7653fe700655/image.png" alt="">
<img src="https://velog.velcdn.com/images/312_log/post/23289db4-60e5-46c7-9b71-fc79f817ff78/image.png" alt="">
TDD의 핵심요소인 테스트코드는 작성할 때 각 유닛에 대해 어떻게 작성해야 하는지 고민하게 된다.
이러한 테스트 코드를 작성할 때 <code>Given-When-Then</code> 패턴을 통해 틀을 잡을 수 있다.</p>
<p>간단한 예제를 통해 알아보도록 하자.</p>
<h3 id="예제">예제</h3>
<p>TestCode.kt</p>
<pre><code>@Nested
@DisplayName(&quot;숫자의 합이 50 이하여야 한다&quot;)
inner class SumValue{

    fun sumNumbers(numA, numB): Int{
        val result = numA + numB
        if (result &gt; 50) throw IllegalStateException()
        return numA + numB
    }

    // 성공
    @ParameterizedTest
    @CsvSource(&quot;20, 20, 40&quot;)
    fun success(inputA, inputB, output) {
        val result = sumNumbers(inputA, inputB)
        assertEquals(output, result)
    }

    // 실패
    @ParameterizedTest
    @CsvSource(&quot;20, 40&quot;)
    fun fail(inputA, inputB){
        assertThrows&lt;IllegalStateException&gt; {
        val result = sumNumbers(inputA, inputB)
    }
}</code></pre><h4 id="given">Given</h4>
<p><code>Given</code>은 테스트를 시작하기 전 주어진 환경을 의미한다.
단순하게 테스트케이스부터 테스트 객체를 초기화하는 전 과정이 포함된다.</p>
<p>위 코드에서 클래스를 만들고 <code>sumNumbers</code>함수 선언, <code>CsvSource</code>로 테스트 케이스를 선언하는 전 과정까지를 <code>Given</code>으로 분류할 수 있다.</p>
<h4 id="when">When</h4>
<p><code>When</code>은 테스트 하고자 하는 유닛의 실행을 의미한다.
일반적으로 가장 짧으며 실행 부분을 담당한다.</p>
<p>위 코드에서 <code>success</code>와 <code>fail</code>을 하는 과정이다.</p>
<h4 id="then">Then</h4>
<p><code>Then</code>은 테스트 결과에 대한 검증이다.
예상되는 결과값에 맞는 데이터가 나왔는지 확인하는 테스트 코드를 작성하는 가장 큰 역할을 담당한다.</p>
<p>위 코드에서 <code>assertEquals</code>나 <code>assertThrows</code>로 예상하는 결과가 나타났는지 확인하는 부분이다.</p>
<h3 id="why-given-when-then">Why Given-When-Then?</h3>
<p>그동안 테스트 코드를 작성할 때, 유닛마다 적용하기 부분이 있어 통일된 기준에 맞춰 관리하기 어려운 부분이 있었다.</p>
<p>그러나 유닛마다 통합된 기준의 적용이 어려운 부분을 감안하고 <code>Given-When-Then</code>패턴을 통해 이러한 형식을 유지하는 방향으로 테스트 코드를 작성했고, 나름 통합된 기준을 제시할 수 있게 되었다..!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Analytics, Crashlytics 을 알아보자]]></title>
            <link>https://velog.io/@312_log/android-Crashlyastics</link>
            <guid>https://velog.io/@312_log/android-Crashlyastics</guid>
            <pubDate>Thu, 23 May 2024 14:23:35 GMT</pubDate>
            <description><![CDATA[<h2 id="analytics">Analytics?</h2>
<p><img src="https://velog.velcdn.com/images/312_log/post/a95537d8-ef31-4bb5-a476-cad2895ecbbc/image.png" alt="">
Google에서 제공하는 <code>Analytics</code>는 출시된 앱의 다양한 보고를 위해 사용된다.</p>
<h2 id="crashlytics">Crashlytics?</h2>
<p>앱을 만들고 스토어에 출시하고 나면 그제서야 보이는 버그들이 있다.</p>
<p><img src="https://velog.velcdn.com/images/312_log/post/39926ec1-8f96-49a7-a24e-4c6ab0dfa5b2/image.png" alt=""></p>
<p>미리 발견한다면 괜찮지만 발견하지 못한 오류가 충돌을 일으켜 앱이 종료되는 경우 이용자들에게서 <code>Crashlytics</code>을 통해 충돌 오류 로그를 받아 볼 수 있다.</p>
<h2 id="how-to-analytics-crashlytics">How to Analytics, Crashlytics</h2>
<p>먼저 Firebase에 앱을 등록해줘야 한다.
<a href="https://console.firebase.google.com/">https://console.firebase.google.com/</a> 으로 이동해
<img src="https://velog.velcdn.com/images/312_log/post/c721385e-aeaf-4945-bf8a-d519011fc0ac/image.png" alt=""></p>
<p>프로젝트를 생성해주고 절차에 따라 진행해준다.</p>
<p>SHA-1 키는 <code>gradle</code>의 <code>signingReport</code>을 통해 얻을 수 있다.</p>
<p>그리고
<img src="https://velog.velcdn.com/images/312_log/post/d8e64521-fd8b-445f-806e-03f3001a8a75/image.png" alt=""></p>
<p>해당 앱의 설정 페이지에서 sdk를 다운받아 앱의 app경로 바로 아래에 위치시켜 준다. 
app 경로는 project 상태에서 볼 수 있다.
<img src="https://velog.velcdn.com/images/312_log/post/0ae8955d-891f-445c-b600-72b2ea724ee1/image.png" alt=""></p>
<p>gradle.kts(Module)</p>
<pre><code>implementation(&quot;com.google.firebase:firebase-crashlytics:19.0.1&quot;)
implementation(&quot;com.google.firebase:firebase-analytics:22.0.1&quot;)</code></pre><p>마지막으로 그래들에 <code>analtyics</code>와 <code>crashlytics</code>을 추가해주면 된다.</p>
<h2 id="analytics-이벤트-추적">Analytics 이벤트 추적</h2>
<p><img src="https://velog.velcdn.com/images/312_log/post/f7f26a3b-2c31-4099-9eb4-c6bcc3915007/image.png" alt="">
별도로 코드를 작성하지 않아도 주기적으로 오류 보고를 보내기 때문에 <code>Analytics</code>에 등록이 된다.</p>
<p>하지만 추가적인 정보나 로그가 필요한 경우 <code>Activity</code>의 <code>onCreate()</code>에서 <code>FirebaseAnalytics.getInstance()</code>를 호출하여 <code>Firebase Analytics</code>를 활성화 한다.</p>
<p>활성화된 인스턴스를 활용해 로그 이벤트를 남길 수 있다.</p>
<ol>
<li>번들에 <code>putString()</code>, <code>putInt()</code> 등으로 로그로 남기길 원하는 데이터들을 넣어주어 내보내는 방법<pre><code>val bundle = Bundle().apply {
 putString(&quot;screen_name&quot;, &quot;{screenName}&quot;)
}
</code></pre></li>
</ol>
<p>firebaseAnalytics.logEvent(FirebaseAnalytics.Event.SCREEN_VIEW, bundle)</p>
<pre><code>2. `AnalyticsKt` 확장함수 이용하기
</code></pre><p>public inline fun FirebaseAnalytics.logEvent(
    name: kotlin.String, crossinline block: ParametersBuilder.() -&gt; kotlin.Unit): kotlin.Unit {
        /* compiled code */ 
}</p>
<pre><code>
따라서 다음과 같이 파라미터빌더를 활용해 간단하게 보낼 수 있다.
</code></pre><p>firebaseAnalytics.logEvent(FirebaseAnalytics.Event.SCREEN_VIEW) {
    param(&quot;screen_name&quot;, &quot;{screenName}&quot;)
}</p>
<pre><code>
## Crashlytics 충돌 추적

![](https://velog.velcdn.com/images/312_log/post/d1c7b006-505b-40e9-906c-2964809feded/image.png)
Firebase Console에서 해당 앱의 실행 탭에서 Crashlytics 항목에 접근하면 앱 이용의 비정상 종료 상황에 대한 로그를 보고받을 수 있다.

![](https://velog.velcdn.com/images/312_log/post/32d88950-abfb-4c2f-b0ba-5db0d9f2473e/image.png)
에러를 누르면 로컬 `Logger`에서 보는것처럼 어디서 에러가 발생했는지, 어떤 에러인지 심지어 커스텀 에러 로그 메시지도 보고 받을수 있었다.

심지어 IDE버전 electric eel이후 부터는 직접적인 연결을 통해 바로바로 확인할 수 있다..!
![](https://velog.velcdn.com/images/312_log/post/1b3ff49c-a0a4-41f2-a82e-0f8f81bad739/image.png)

회사에서는 B2B서비스만 작업했기 때문에 문제가 생기면 고객사에서 바로 연락을 줬었다. 

그때마다 `Crashlytics`를 뒤져가며 오류를 찾았는데, 커스텀 로그메시지를 조금 더 일찍 알았더라면 깔끔한 대처가 가능했을 것 같다..! 

[해당 velog를 참고했습니다.](https://velog.io/@cksgodl/Android-Firebase-Analytics-%EB%B0%8F-Crashlytics%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%B4-%EC%95%B1-%EC%82%AC%EC%9A%A9%EC%9E%90%EC%97%90-%EB%8C%80%ED%95%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-%EC%88%98%EC%A7%91%ED%95%B4%EB%B3%B4%EC%9E%90)</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[LiveData와 StateFlow을 알아보자]]></title>
            <link>https://velog.io/@312_log/kotlin-coroutine</link>
            <guid>https://velog.io/@312_log/kotlin-coroutine</guid>
            <pubDate>Wed, 22 May 2024 13:12:37 GMT</pubDate>
            <description><![CDATA[<p>기존에는 <code>LiveData</code>를 <code>ViewModel</code>에서 선언하고 사용하는 클래스에서 <code>observer</code>을 통해 <code>LiveData</code>의 상태 변화를 감지하는 방식을 애용했다.</p>
<p>최근에는 <code>StateFlow</code>를 활용해 <code>UI</code>를 만들었는데 어떤 차이가 있는지 알아보자.</p>
<h3 id="livedata">LiveData?</h3>
<p><img src="https://velog.velcdn.com/images/312_log/post/29c4f18d-1f5a-4c93-8b6d-7ca96b3e94e6/image.png" alt="">
<code>LiveData</code>는 수명 주기를 인식해서 필요한 클래스의 <code>observer</code>에서 관찰하고 클래스의 수명이 끝나면 <code>observer</code>도 함께 <code>destroy</code> 된다.</p>
<p>MVVM에서의 <code>LiveData</code>는 <code>ViewModel</code> 에서 선언하고 관리하며, <code>View</code>에서 <code>LiveData</code>의 변화를 <code>observe</code> 해서 UI에 반영해준다.</p>
<h3 id="why-stateflow">Why StateFlow?</h3>
<p><img src="https://velog.velcdn.com/images/312_log/post/40a8b2f1-49ee-4852-8a28-fc59ce81e8c8/image.png" alt="">
그렇다면 <code>StateFlow</code>는 어떤 점이 다를까?</p>
<h3 id="변경되지-않은-데이터의-관찰">변경되지 않은 데이터의 관찰</h3>
<p>먼저 <code>StateFlow</code>는 값의 변화가 없다면 업데이트 되지 않는다.</p>
<p><code>LiveData</code></p>
<pre><code>private val _liveDataNumber = MutableLiveData&lt;Int&gt;(0)
val liveDateNumber = _liveDataNumber

fun number() {
    _liveDataNumber.value = _liveDataNumber.value
}</code></pre><p><code>StateFlow</code></p>
<pre><code>private val _stateFlowNumber = MutableStateFlow&lt;Int&gt;(0)
val stateFlowNumber = _stateFlowNumber.asStateFlow()

fun number() {
    _stateFlowNumber.value = _stateFlowNumber.value
}</code></pre><p>두가지 같은 코드를 <code>LiveData</code>와 <code>StateFlow</code>로 작성했을 때, 
<code>nubmer</code> 함수를 호출했을때 로그를 찍어보았다.
<img src="https://velog.velcdn.com/images/312_log/post/07891a6b-bbc0-4b78-91dc-eda579301095/image.png" alt=""></p>
<p>처음 값을 할당할 때에는 둘다 로그가 찍히지만, 
이후에 <code>number</code> 함수를 호출해주자 <code>LiveData</code>만 로그에 나타난다.</p>
<h3 id="flow를-활용한-안정된-비동기-스트림">Flow를 활용한 안정된 비동기 스트림</h3>
<p><code>Flow</code>는 값을 순차적으로 방출하고 완료되는 비동기 데이터 스트림이기 때문에 <code>Data Layer</code>(ex: <code>Repository</code>, <code>Room</code>)에서도 쓸 수 있다. </p>
<p>또한, 코틀린의 코루틴 API이기 때문에 안드로이드 플랫폼에 종속적이지도 않다.</p>
<h3 id="hot-stream">Hot Stream</h3>
<p>LiveData와는 다른 Hot Stream이기 때문에 collect하는 클래스가 없어도 항상 데이터를 최신으로 유지하고 업데이트한다.</p>
<p>항상 안전한 최신 데이터를 보낼 수 있다..!</p>
<p>이외에도 초기값을 무조건 할당해야하고, 코루틴 내에서 보다 편하게 사용가능 하다는 등의 차별점이 있다.</p>
<h2 id="livedata-사용해보기">LiveData 사용해보기</h2>
<p>간단하게 사용하는 방법을 알아보겠다.</p>
<p>ViewModel</p>
<pre><code>private val _liveDataText = MutableLiveData&lt;Int&gt;(&quot;&quot;)
val liveDataText = _liveDataText

fun updateText(text: String) {
    _liveDataText.value = text
}</code></pre><p>Activity</p>
<pre><code>lifecycleScope.launch { 
    viewModel.liveDateText.observe(this@MainActivity){
        Log.d(TAG,&quot;liveDateText: $it&quot;)
    }
}</code></pre><p>lifecycleScope을 열어서 observer을 할당해준다.
이후에는 값이 업데이트 될때마다 로그에 나타나게 된다.</p>
<h2 id="stateflow-사용해보기">StateFlow 사용해보기</h2>
<p>ViewModel</p>
<pre><code>private val _stateFlowText = MutableStateFlow&lt;String&gt;(&quot;&quot;)
val stateFlowText = _stateFlowText.asStateFlow()

fun updateText(text: String) {
    _stateFlowText.value = text
}</code></pre><p>Activity</p>
<pre><code>lifecycleScope.launch {
    viewModel.stateFlowText.collect {
        Log.d(TAG,&quot;stateFlowText: it)
    }
}</code></pre><p>Compose에서는 더 간단하게 활용이 가능하다.</p>
<h3 id="stateflow">StateFlow!</h3>
<p>기존에는 LiveData만 활용한 개발을 했기 때문에 굳이 바꿔야하나 싶기도 했고 소규모 서비스였기에 데이터 스트림에 관한 문제도 겪지 못했기 때문에 필요성을 느끼지 못했었다.</p>
<p>하지만 Compose에서도 적용시켜보고 앞으로 StateFlow가 많은 개선점들 덕분에 주류가 되어가고 있기에 학습해보고 싶었다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kotlin Coroutine을 알아보자]]></title>
            <link>https://velog.io/@312_log/Kotlin-Coroutine</link>
            <guid>https://velog.io/@312_log/Kotlin-Coroutine</guid>
            <pubDate>Tue, 21 May 2024 07:04:03 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/312_log/post/bd7dd703-c8ba-4c96-9fcf-25e5b40bc838/image.png" alt="">
(<a href="https://developer.android.com/kotlin/coroutines?hl=ko">https://developer.android.com/kotlin/coroutines?hl=ko</a>)</p>
<p>오늘은 Kotlin에서의 Coroutine을 알아보자.
Coroutine은 정말 중요하고 깊이있는 주제인 만큼 이 글에서는 간단하게만 소개하려고 한다.</p>
<h2 id="what-is-coroutine">What is Coroutine?</h2>
<p>개발에서 Coroutine은 비동기적인 프로그래밍을 할때 사용한다.</p>
<p>Andorid에서는 통신 중에 응답을 기다리거나 DB 접근, 입출력 등의 다양한 동시작업에서 활용하게 된다.</p>
<p>이러한 작업을 동시성 없이 처리하게 되면, 응답을 받기까지 메인 스레드 블로킹이 발생해 UI가 멈추거나 앱 전체가 응답 없음 상태가 된다.</p>
<p>Kotlin Coroutine은 비동기 프로그래밍을 단순하고 효율적으로, 비동기 작업을 동기 코드처럼 작성하게 해준다.</p>
<h2 id="coroutine-scope">Coroutine Scope?</h2>
<p>CoroutineScope는 Coroutine을 관리하는 범위다.</p>
<p>모든 Coroutine은 Scope내에서 실행되어야 하며 GlobalScope와 CoroutineScope, ViewModelScope가 존재한다.</p>
<p>GlobalScope는 앱의 생명주기와 함께 동작해 장시간 실행되는 Coroutine에 적합하다.</p>
<p>반대로 CoroutineScope는 통신이나 DB등의 단기간 작업의 생명 주기와 함께 동작할 때 쓰인다.</p>
<p>ViewModelScope는 Jetpack의 ViewModel이라는 컴포넌트의 생명주기와 함께 사용된다.</p>
<h2 id="what-is-dispatcher">What is Dispatcher?</h2>
<p>CoroutineScope의 경우에는 Dispatcher을 지정해 줄 수 있다.
Dispatcher은 4가지 타입으로 각 상황에 효율적인 스레드를 지정할 수 있다.</p>
<p>Dispatchers.Default: 
일반적인 작업에 사용하며 CPU를 많이쓰는 연산 작업에 특화되어있다.</p>
<p>Dispatchers.IO: 
통신이나 파일의 입출력, 통신이나 DB에 적합하다.</p>
<p>Dispatchers.Main: 
안드로이드 UI등을 담당하는 메인 스레드이다.</p>
<p>Dispatchers.Unconfined: 
호출한 context를 기반으로 중단 후 재실행될때 재실행한 context를 사용하는 Dispatcher이다.</p>
<h2 id="how-to-coroutine">How To Coroutine?</h2>
<p>coroutine을 시작하는 블록은 launch와 async 두가지 블록이 존재한다.</p>
<h4 id="실행-메소드">실행 메소드</h4>
<p>launch는 상태 관리,결과를 반환하지 않는 Coroutine을 실행한다. (Job 반환)</p>
<p>async는 상태도 관리하지만 결과도 반환하는 Coroutine을 실행한다 (Deferred 반환)</p>
<h4 id="상태-관리-메소드">상태 관리 메소드</h4>
<p>cancel은 coroutine을 정지시키며 하위 coroutine까지 모두 정지시킨다.</p>
<p>delay는 지정된 시간만큼만 일시 중단한다.</p>
<pre><code>delay(1000L)</code></pre><p>join은 coroutine 내부의 여러 블록이 존재할 때 join의 Job을 완료된 후 실행시킨다.</p>
<h2 id="활용-예시">활용 예시</h2>
<pre><code>fun coroutineLoading(
        dispatcher: CoroutineDispatcher = Dispatchers.IO,
        operation: suspend () -&gt; Unit
    ) {
        viewModelScope.launch(dispatcher) {
            try {
                startLoading()
                operation()
            } catch (e: Exception) {
                e.message?.let { showError(it) }
            } finally {
                finishLoading()
            }
        }
    }</code></pre><p>모든 ViewModel을 사용하는 BaseViewModel에 작성한 함수이다.
파라미터로 어떤 코루틴을 사용할건지, 어떤 함수를 사용할건지 받고 기능을 사용할 때 로딩 시작을, 끝낼 때 로딩을 종료해서 UI에 로딩 화면이 보여지게 작성해봤다.</p>
<h2 id="coroutine">Coroutine!</h2>
<p>이외에도 다양한 coroutine의 메소드들이 존재하고 Kotlin Coroutine은 이를 통해 비동기 프로그래밍을 간단하게 만들며, 가독성 높은 코드를 작성할 수 있게 한다.</p>
<p>사용할때는 단순히 통신이나 DB같은 응답이 필요한 부분에만 기본적인 기능으로 적용했지만, UI 렌더링이나 이미지 프리로딩 등 좀 더 효율적인 구조에 활용해볼 수 있겠다고 생각한다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Android DataStore를 알아보자]]></title>
            <link>https://velog.io/@312_log/kotlin-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0</link>
            <guid>https://velog.io/@312_log/kotlin-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0</guid>
            <pubDate>Mon, 20 May 2024 14:08:48 GMT</pubDate>
            <description><![CDATA[<h2 id="what-is-datastore">What is DataStore?</h2>
<p><img src="https://velog.velcdn.com/images/312_log/post/25d0f983-5d8f-45ee-9dbf-e434f509723b/image.png" alt="">
데이터를 저장할 때 사용하는 안드로이드 내부 로컬 DB이다.
이전에는 SharedPreference를 사용했는데, DataStore로의 이전이 권고되었다.</p>
<h2 id="why-datastore">Why DataStore?</h2>
<p>DataStore 이전에 로컬 Database를 사용할 때에는 SharedPreference 라는 인터페이스를 사용했었다. </p>
<p>간단한 key-value를 저장할 때는 유용했지만, 멀티 쓰레드나 타입의 안정성 등의 이유로 DataStore가 등장하게 되었다고 한다.</p>
<p>따라서 DataStore는 비동기적이고 타입 안정성을 제공하며 데이터 일관성을 보장한다.</p>
<h2 id="datastores-advantages">DataStore&#39;s Advantages</h2>
<p>DataStore는 SharedPreference에 존재하던 여러가지 단점이 개선되었다.</p>
<p>먼저 정식으로 Kotlin Coroutine과 Flow타입을 지원해 동시성 프로그래밍에 최적화 되었다. </p>
<p>그리고 데이터 일관성 보장을 통해 멀티 쓰레드 환경에서 동일하게 데이터를 추출할 수 있게 되었다.</p>
<p>마지막으로 SharedPreference에는 없던 타입 안정성을 제공해서 데이터가 클래스를 기준으로 입출력 되어 안전하게 I/O 할 수 있다.</p>
<h2 id="use-datastore">Use DataStore!</h2>
<p>SharedPreference에 비해 DataStore는 비동기적 데이터 처리, Coroutine 및 Flow 지원, 데이터 일관성 보장 등의 강력한 기능을 제공한다. </p>
<p>따라서 기존에 적용되어 있던 프로젝트를 리팩토링하거나 새로운 프로젝트를 시작할 때, DataStore로 전환하게 되면 앱의 안정성과 에러 관리에 있어서 더 용이할 것 같다고 생각한다.</p>
<p>특히 Google의 Jetpack에 대한 지원을 통해 DataStore의 채택률이 더 올라갈 것 같다..!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Android Jetpack을 알아보자]]></title>
            <link>https://velog.io/@312_log/Kotlin-Jetpack</link>
            <guid>https://velog.io/@312_log/Kotlin-Jetpack</guid>
            <pubDate>Sun, 19 May 2024 13:11:21 GMT</pubDate>
            <description><![CDATA[<p>Android에서는 개발을 돕는 다양한 종류의 라이브러리가 존재한다.
이번에는 Android에서 공식적으로 제공하는 Jetpack이라는 라이브러리 묶음에 대해 알아보겠다.
<img src="https://velog.velcdn.com/images/312_log/post/80c15022-b516-4bc4-9b06-6e6706647975/image.png" alt=""></p>
<h2 id="jetpack이란">Jetpack이란?</h2>
<p><img src="https://velog.velcdn.com/images/312_log/post/ea51dc48-04af-40ce-9c77-a4e6d3b088e5/image.png" alt=""></p>
<p>Jetpack은 Google이 공식적으로 제공하는 라이브러리의 묶음이다.
개발자가 Jetpack을 타고 날아다니는 것처럼 편리하게 개발할 수 있어서 붙여진 별명이라고 한다.</p>
<h2 id="왜-jetpack을-사용해야-할까">왜 Jetpack을 사용해야 할까?</h2>
<p><img src="https://velog.velcdn.com/images/312_log/post/e243ed2e-6268-4df1-bd90-56fa25226bc8/image.png" alt="">
(호환성, 상용코드 제거, 파편화 제거)</p>
<p>먼저 보편적인 개발 환경을 제공한다는 장점이 있다.
다양한 Jetpack의 라이브러리들끼리 높은 호환성을 바탕으로 설계되어있어 고려할 사항이 줄어든다.
그리고 라이브러리들을 공통으로 사용함으로서, 자연스럽게 러닝커브가 낮아지게 되고 작성해야할 보일러플레이트 코드가 줄어드는 효과도 얻을 수 있다.
마지막으로, 구글의 공식 지원을 통해 항상 최신화를 유지하는 것도 이유다..!</p>
<h2 id="jetpack에는-어떤-라이브러리가-있을까">Jetpack에는 어떤 라이브러리가 있을까?</h2>
<p>정말 많은 라이브러리가 존재하기 때문에, 유명하고 일반적으로 많이 쓰이는 라이브러리들을 열거해보겠다.</p>
<ul>
<li>Navigation</li>
</ul>
<p>앱 내의 화면 전환 및 백스택 관리를 위한 라이브러리이다. 
Navitgation을 활용해 단일 액티비티로 개발하기도 한다.</p>
<ul>
<li>Room</li>
</ul>
<p>SQLite 데이터베이스를 이용해 로컬에서 DB를 이용하게 해주는 라이브러리다.
SQL 쿼리 작성 관련 코드를 인식하며, 컴파일 타임에 쿼리를 검증한다.</p>
<ul>
<li>LiveData</li>
</ul>
<p>관찰 가능한 데이터 홀더 클래스로, 데이터 변경 시 자동으로 UI를 업데이트할 수 있다. Observer 패턴 적용에 용이한다. 최근에는 StateFlow를 사용하기도 한다.</p>
<ul>
<li>ViewModel</li>
</ul>
<p>UI 관련 데이터를 저장하고 관리하는 클래스다. 데이터의 생명주기를 액티비티나 프래그먼트와 공유하거나 관리하기 용이하게 LiveData와 함께 운용한다.</p>
<ul>
<li>WorkManager</li>
</ul>
<p>백그라운드 작업을 쉽게 관리할 수 있는 라이브러리다. 네트워크 상태나 기기 충전 상태 등 조건에 따라 작업을 실행할 수 있다.</p>
<ul>
<li>Data Binding</li>
</ul>
<p>XML 레이아웃과 데이터 소스를 연결하여 UI 컴포넌트와 데이터의 동기화를 자동으로 처리할 수 있다. ViewBinding을 사용하기도 한다.</p>
<ul>
<li>Paging</li>
</ul>
<p>대량의 데이터를 페이지 단위로 로드하여 효율적으로 표시할 수 있게 해주는 라이브러리다.</p>
<ul>
<li>Lifecycle</li>
</ul>
<p>액티비티와 프래그먼트의 생명주기를 쉽게 관리할 수 있는 라이브러리다.
액티비티, 프래그먼트의 생명주기 이벤트를 관찰 및 인식하고 이벤트를 작성할 수 있다.</p>
<ul>
<li>CameraX</li>
</ul>
<p>안드로이드의 카메라 기능을 쉽게 사용할 수 있게 해주는 라이브러리다.
카메라 기능과 관련된 동작과 다양한 카메라 기능을 지원해준다.</p>
<ul>
<li>Hilt</li>
</ul>
<p>의존성 주입을 쉽게 구현할 수 있도록 도와주는 라이브러리다. Java의 Dagger를 기반으로 개발되었다.
의존성 관리 간소화, 구성 요소의 생명주기와 통합, 모듈 선언 간소화등의 기존 기능을 kotlin에서 더 편리하게 실행해준다.</p>
<ul>
<li>Security</li>
</ul>
<p>보안 기능을 쉽게 구현할 수 있도록 도와주는 라이브러리다.
안전한 데이터 저장, 암호화, 키 관리를 가능하게 해준다.</p>
<ul>
<li>Compose</li>
</ul>
<p>기존 xml로 작성하던 UI를 선언형과 함수형 프로그램을 통해 제공한다.</p>
<p>이외에도 다양한 종류의 라이브러리가 Jetpack 내부에 내장되어있다!</p>
<p>Jetpack을 되짚어보며 몰랐던 라이브러리도 있었고 너무 기초적인 라이브러리라 안드로이드 내장 함수라고 생각했던 라이브러리도 있었다.
구글에서 공식적으로 지원하고 홍보하는만큼 신뢰성있으니 꾸준히 배워야겠다고 생각했다..!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Hilt - compose로 RetroFit 사용해보기]]></title>
            <link>https://velog.io/@312_log/Hilt-compose%EB%A1%9C-RetroFit-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@312_log/Hilt-compose%EB%A1%9C-RetroFit-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sun, 21 Apr 2024 13:16:39 GMT</pubDate>
            <description><![CDATA[<p>이번에는 RetroFit 통신 라이브러리를 compose에 적용해보겠다.</p>
<h2 id="1-retrofit-환경-구축">1. RetroFit 환경 구축</h2>
<pre><code>dependencies {
    ...
    // Retrofit
    implementation(&quot;com.squareup.retrofit2:retrofit:2.9.0&quot;)
    implementation(&quot;com.squareup.okhttp3:logging-interceptor:3.11.0&quot;)
    implementation(&quot;com.squareup.retrofit2:converter-gson:2.9.0&quot;)
    implementation(&quot;com.squareup.retrofit2:converter-scalars:2.9.0&quot;)
    ...
}</code></pre><p>retroFit에 필요한 환경들을 import 해준다.
두번째 줄은 통신할때마다 log를 가져오기 위해서, 
셋째 줄과 넷째 줄은 google 로그인과 gson 파싱을 위해 추가해줬다.</p>
<h2 id="2restful-module-만들기">2.Restful Module 만들기</h2>
<p>RestfulModule</p>
<pre><code>@Module
@InstallIn(SingletonComponent::class)
object RestfulModule {

    @Singleton
    @Provides
    fun provideHttpLoggingInterceptor(): HttpLoggingInterceptor {
        return HttpLoggingInterceptor(HttpLoggingInterceptor.Logger.DEFAULT).apply {
            level = if (com.google.firebase.BuildConfig.DEBUG) {
                HttpLoggingInterceptor.Level.BODY
            } else {
                HttpLoggingInterceptor.Level.NONE
            }
        }
    }

    @Singleton
    @Provides
    fun provideOkHttpClient(
        httpLoggingInterceptor: HttpLoggingInterceptor
    ): OkHttpClient {
        return OkHttpClient.Builder()
            .addInterceptor(httpLoggingInterceptor)
            .addNetworkInterceptor { chain -&gt;
                chain.proceed(chain.request().newBuilder().build())
            }
            .connectTimeout(5000L, TimeUnit.MILLISECONDS)
            .readTimeout(5000L, TimeUnit.MILLISECONDS)
            .writeTimeout(5000L, TimeUnit.MILLISECONDS)
            .build()
    }

    @Singleton
    @Provides
    fun provideRetrofit(
        okHttpClient: OkHttpClient,
    ): Retrofit {
        val gson = GsonBuilder().setLenient().create()

        return Retrofit.Builder()
            .addConverterFactory(GsonConverterFactory.create(gson))
            .addConverterFactory(ScalarsConverterFactory.create())
            .client(okHttpClient)
            .baseUrl(BuildConfig.BASE_URL)
            .build()
    }
}</code></pre><p>Module은 처음 환경을 구축할 때 다른 개발자 분 벨로그를 많이 참고했었다.</p>
<p>먼저 저번 Room처럼 @Module와 @InstallIn을 통해 Hilt에 선언해준다.</p>
<p>provideHttpLoggingInterceptor은 Request와 Response의 로그를 관리해준다.
개발 과정에서 진짜 진짜 중요하고 덕분에 편하게 개발할 수 있었다..!
관련 라이브러리로 chucker도 있는데, 나중에 포스팅 해보겠다.</p>
<p>provideOkHttpClient는 위에서 생성한 logInterceptor을 추가하고 통신 관련 규약을 작성한 Retrofit에 들어갈 Client이다.
현재는 타임아웃을 5초로 생성해줬다.</p>
<p>마지막으로 provideRetrofit은 Retrofit을 반환하며 위에서 생성한 클라이언트 연결, gson변환과 baseUrl 연동을 해줬다.</p>
<p>AppModule.kt</p>
<pre><code>@Module
@InstallIn(SingletonComponent::class)
object AppModule {
    ...

    @Singleton
    @Provides
    fun provideDiaryDao(diaryDatabase: DiaryDatabase): DiaryDao = diaryDatabase.diaryDao()

    @Singleton
    @Provides
    fun provideDiaryService(retrofit: Retrofit): DiaryService {
        return retrofit.create(DiaryService::class.java)
    }

    @Singleton
    @Provides
    fun provideDiaryRepository(
        diaryDao: DiaryDao,
        diaryService: DiaryService
    ): DiaryRepository {
        return DiaryRepository(diaryDao, diaryService)
        ...
    }</code></pre><p>마지막으로 저번에 생성한 AppModule에 Dao와 함께 DiaryService를 Repository에 연결해주면 된다.</p>
<h2 id="3-dto-service-만들기">3. DTO, Service 만들기</h2>
<p>DiaryDetailDTO</p>
<pre><code>data class DiaryDetailDTO(
    @SerializedName(&quot;title&quot;)
    val title: String,
    @SerializedName(&quot;content&quot;)
    val content: String,
    @SerializedName(&quot;date&quot;)
    val date: String,
    @SerializedName(&quot;location&quot;)
    val location: SeoulLocation,
)</code></pre><p>data class 로 통신할 때 사용할 모델을 만들어준다.
Request와 Response 모델이 있고 Response로 사용할 DiaryDeatilDTO를 활용해보겠다.</p>
<p>@SerializedName 은 Gson에서 내부의 문자열을 Json 키값으로 사용해준다.
API문서 그대로 입력해주면 된다.</p>
<p>DiaryService.kt</p>
<pre><code>interface DiaryService {

    // 일기 작성
    @POST(&quot;/diary&quot;)
    suspend fun diaryWrite(
        @Header(&quot;token&quot;) token: String,
        @Body diary: Diary
    ): Response&lt;Unit&gt;

    // 일기 조회
    @GET(&quot;/diary/{id}&quot;)
    suspend fun diaryDetail(
        @Header(&quot;token&quot;) token: String,
        @Path(&quot;id&quot;) id: String
    ): Response&lt;DiaryDetailDTO&gt;
}</code></pre><p>interface로 Service를 만들어준다.
이 클래스를 통해 일기와 관련된 API들을 관리하고 호출해준다.
(함수명은 보통 동사가 앞에오지만 정렬을 위해 diary를 의도적으로 앞에 뒀다.)</p>
<p>@POST나 @GET는 RESTFUL의 4가지 형태중 하나로, API가 어떤 역할을 수행할지 나타내준다.
그 뒤에는 API의 BaseURL 뒤의 세부 주소를 입력해준다.</p>
<p>suspend로 선언해주는 이유는 ROOM DAO와 마찬가지로 응답이 올때까지 비동기적으로 함수를 수행해주기 위해서다.</p>
<p>이후에는 API에서 요구하는 대로 파라미터를 넣어주면 된다.
토큰을 이용하는 통신을 가정하고 Header에 토큰을 넣어줬고 일기를 작성하는 API에는 Body 로 Diary 모델을, 조회하는 API에는 일기의 ID값을 넣어줬다.</p>
<p>@GET으로 지정된 API는 결과값을 리턴받게 된다.
일기 조회의 경우에는 앞서 생성한 DiaryDetailDTO를 Response로 받을 수 있다.</p>
<h2 id="4-repository에서-호출하기">4. Repository에서 호출하기</h2>
<p>DiaryRepository</p>
<pre><code>class DiaryRepository @Inject constructor(
    private val diaryDao: DiaryDao,
    private val diaryService: DiaryService
) : BaseRepository() {

    val diaries: Flow&lt;List&lt;Diary&gt;&gt; = diaryDao.getAll().flowOn(Dispatchers.IO).conflate()
    val accessToken = &quot;token&quot;

    // DB에 일기 입력하는 함수
    suspend fun insertDiaryDao(diary: Diary) = diaryDao.insertDiaries(diary)

    // 통신이 성공했을 때만 DB 입력 함수를 호출
    suspend fun writeDiary(diary: Diary) {
        diaryService.diaryWrite(accessToken, diary).let {
            if (it.isSuccessful) insertDiaryDao(diary)
        }
    }

    // 일기 조회 함수
    suspend fun readDiary(diaryId: String): Diary? {
        diaryService.diaryDetail(accessToken, diaryId).let {
            if (it.isSuccessful) {
                val response = it.body()!!
                return Diary(
                    title = response.title,
                    message = response.content,
                    date = Formatter.longToLocalDateTime(response.date.toLong()),
                    location = response.location
                )
            }
        }
        return null
    }
}</code></pre><p>지난번에 생성한 DB에 일기를 입력하는 함수를 이번에 만든 함수의 통신 결과가 성공했을 때만 호출되게 변경했다. 
항상 최신 DB의 정보를 가져오는 Flow타입의 diaries 변수의 서버와 내부 로컬 DB가 달라질 경우 사용자의 혼란이 생길 수 있기 때문이다. </p>
<p>일기 조회 함수의 경우 방식은 동일하지만 일기 id를 받아서 상세정보를 반환하고 통신이 실패한 경우는 null을 반환해 에러를 알 수 있게 했다.
(통신 에러인지 확인할 수 있게 더 꼼꼼하게 작업해줘야 한다.)</p>
<p>DiaryDetailDTO는 API에서 사용하는 통신모델이기 때문에 더 이상 혼용되지 않도록 Repository에서 내부에서 사용할 Diary 모델로 변경해줬다.
(Formatter는 자체적으로 만든 Util클래스)</p>
<p>이후에는 viewModel에서 함수들을 호출해주면 된다.</p>
<p>// 느낀점</p>
<p>RetroFit을 처음 구축하기까지 정말 고생을 많이 했었다..
공식 문서부터 구글링과 블로그들을 따라해보고 응용해보면서 Hilt가 의존성을 정말 편하게 관리해주고 있다는 것과 Retrofit이 나오기 전까지 HttpConnection과 OkHttp를 거쳐가면서 선배들의 수많은 고충들을 감히 예상해 볼 수 있었다.</p>
<p>Hilt-Compose 초간단 일기 앱 마무리!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Hilt - compose로 Room 사용해보기]]></title>
            <link>https://velog.io/@312_log/Hilt-compose%EB%A1%9C-Room-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@312_log/Hilt-compose%EB%A1%9C-Room-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Mon, 15 Apr 2024 02:38:40 GMT</pubDate>
            <description><![CDATA[<p>지난 글에서 만든 Hilt 프로젝트를 바탕으로 일기장 예제 앱을 만들어보겠다.</p>
<h2 id="1-room-환경-구축">1. Room 환경 구축</h2>
<p>App Gradle</p>
<pre><code>dependencies {
    ...
    // Room
    implementation(&quot;androidx.room:room-runtime:2.6.1&quot;)
    ksp(&quot;androidx.room:room-compiler:2.6.1&quot;)
    implementation(&quot;androidx.room:room-ktx:2.6.1&quot;)
    ...
    }</code></pre><p>현재 환경에 맞는 Room 버전을 추가해준다.</p>
<h2 id="2-entity-dao-database-만들기">2. Entity, Dao, DataBase 만들기</h2>
<p>Diary</p>
<pre><code>@Entity(tableName = &quot;diary&quot;)
data class Diary(
    @ColumnInfo(name = &quot;diary_title&quot;) var title: String = &quot;&quot;,
    @ColumnInfo(name = &quot;diary_date&quot;) var date: LocalDateTime = LocalDateTime.now(),
    @ColumnInfo(name = &quot;diary_content&quot;) var content: String = &quot;&quot;,
) {
    @PrimaryKey(autoGenerate = true)
    var id: Long = 0

    override fun toString(): String {
        return &quot;id = $id, name = $title, content = $content
    }
}</code></pre><p>@Entity 어노테이션을 통해 SQLite에서 인식할 tableName을 정해준다.
이름을 설정해주지 않으면 클래스 이름으로 table이 생성된다.</p>
<p>@ColumnInfo 어노테이션으로 호출할 네이밍을 설정할 수 있다.
마찬가지로 설정해주지 않으면 변수명으로 생성된다.</p>
<p>@PrimaryKey 어노테이션은 이 ID를 SQLite에서 고유한 id로 설정하겠다는 것을 의미한다. AutoGenerate 기능을 활용하려면 Long, Int타입으로 지정해줘야 한다.
AutoGenerate를 true로 설정하면 따로 입력해주지 않아도 자동으로 고유값을 생성한다.</p>
<p>DiaryDao</p>
<pre><code>@Dao
interface DiaryDao {

    // 생성
    @Insert
    fun insertDiary(diary: Diary)

    // 삭제
    @Query(&quot;DELETE FROM diary where id = :id&quot;)
    fun deleteDiary(id: Long)

    @Query(&quot;DELETE FROM diary&quot;)
    fun deleteAll()

    // 업데이트
    @Query(&quot;UPDATE diary SET diary_title = :title WHERE id = :id&quot;)
    fun updateTitle(id: Long, title: String)

    @Query(&quot;UPDATE diary SET diary_content = :content WHERE id = :id&quot;)
    fun updateContent(id: Long, content: String)

    @Query(&quot;UPDATE diary SET diary_date = :date WHERE id = :id&quot;)
    fun updateDate(id: Long, date: LocalDateTime)

    // 탐색
    @Query(&quot;SELECT * FROM diary&quot;)
    fun getAll(): Flow&lt;List&lt;Diary&gt;&gt;

    @Query(&quot;SELECT * FROM diary where id = :id&quot;)
    fun getDiary(id: Long): Diary</code></pre><p>@Dao 어노테이션으로 선언해주고 필요한 쿼리들을 생성해준다.</p>
<p>getAll 함수에서 Flow 타입으로 받는 이유는 많은 데이터를 송수신할때 발생할 수 있는 에러나 최신 데이터 검증 등을 안전하게 관리하게 위해서 사용해준다.</p>
<p>DiaryDatabase</p>
<pre><code>@Database(entities = [Diary::class], version = 1, exportSchema = false)
@TypeConverters(ListConverters::class)
abstract class DiaryDatabase : RoomDatabase() {
    abstract fun diaryDao(): DiaryDao
}</code></pre><p>@Database 어노테이션으로 Database임을 선언한다.</p>
<p>entites는 어떤 entity를 활용하는지 Database에 알려준다.</p>
<p>version은 현재 Database나 entity가 변경될때마다 버전을 1씩 올려주면 된다.
버전 기록을 유지하고 싶지 않으면 비활성화 할 수 있다.</p>
<p>exportSchema는 자동 마이그레이션을 활용할 때 사용한다.
구조나 Entity가 변경될 때 기존 데이터를 활용할지 모두 삭제할지 정할 수 있다.</p>
<p>@TypeConverters는 DB가 인식하지 못하는 타입을 변환할 클래스를 입력한다.</p>
<p>ListConverters</p>
<pre><code>class ListConverters {
    @TypeConverter
    fun listToJson(value: List&lt;String?&gt;): String? {
        return Gson().toJson(value)
    }

    @TypeConverter
    fun jsonToList(value: String): List&lt;String&gt;? {
        return Gson().fromJson(value, Array&lt;String&gt;::class.java)?.toList()
    }

    @TypeConverter
    fun longToDate(value: String?): LocalDateTime? {
        return value?.let { LocalDateTime.parse(it) }
    }

    @TypeConverter
    fun dateToLong(date: LocalDateTime?): String? {
        return date?.toString()
    }    
}</code></pre><p>Database에서 지원되지 않는 List나 LocatDateTime 타입 등은 Database가 인식할 수 있는 작업이 병행되어야 한다. 
Database에서 데이터를 꺼내고 다시 원래 타입으로 변환하는 작업도 필요하기 때문에 변환 함수를 @TypeConveter 어노테이션으로 선언하고 만들어준다.</p>
<h2 id="3-repository-생성하기">3. Repository 생성하기</h2>
<p>DiaryRepository</p>
<pre><code>class DiaryRepository @Inject constructor(
    private val diaryDao: DiaryDao
) {
    val diaries: Flow&lt;List&lt;Diary&gt;&gt; = diaryDao.getAll().flowOn(Dispatchers.IO).conflate()

    suspend fun insertDiaryDao(diary: Diary) = diaryDao.insertDiary(diary)

    suspend fun deleteDiaryDao(id: Long) = diaryDao.deleteDiary(id)

    suspend fun deleteAllDao() = diaryDao.deleteAll()

    suspend fun updateDiaryDao(
        id: Long,
        title: String? = null,
        content: String? = null,
        date: LocalDateTime? = null,
    ) {
        title?.let { diaryDao.updateTitle(id, it) }
        content?.let { diaryDao.updateContent(id, it) }
        date?.let { diaryDao.updateDate(id, it) }
    }

    suspend fun getAllDao(): Flow&lt;List&lt;Diary&gt;&gt; =
        diaryDao.getAll().flowOn(Dispatchers.IO).conflate()

    suspend fun getDiaryDao(id: Long): Diary =
        diaryDao.getDiary(id)
}</code></pre><p>Repository에서는 @Inject로 생성자를 주입해주면 diaryDao에서 생성해준 함수들을 사용할 수 있다. suspend fun으로 함수를 생성해서 비동기 함수임을 알려준다.</p>
<h2 id="4-appmodule에-주입해주기">4. AppModule에 주입해주기</h2>
<p>AppModule</p>
<pre><code>@Module
@InstallIn(SingletonComponent::class)
object AppModule {

    @Singleton
    @Provides
    fun provideDiaryDao(diaryDatabase: DiaryDatabase): DiaryDao = diaryDatabase.diaryDao()

    @Singleton
    @Provides
    fun provideDiaryRepository(diaryDao: DiaryDao): DiaryRepository = DiaryRepository(diaryDao)
}</code></pre><p>AppModule은 앱 내의 Hilt 의존성을 관리하는 클래스로 생성했다.</p>
<p>@Module 어노테이션으로 Hilt 앱 내의 어떤 컴포넌트에 주입되는지 알려줄 수 있다.</p>
<p>@InstallIn은 Module과 함께 어디서 이 모듈안의 객체가 생성될것인지 정해줄 수 있다.</p>
<p>@Singleton을 통해 싱글톤임을 선언한다.</p>
<p>@Provides는 @Binds를 사용할 수 없는 외부 클래스 등을 주입할 때 사용한다.
Room을 사용하기 위한 예제이므로 Provides를 사용했다.
기본적으로 null을 허용하지 않으며 값을 return하지 않는다면 @Provides를 사용할 수 없다.</p>
<h2 id="5-viewmodel에서-호출하기">5. ViewModel에서 호출하기</h2>
<p>DiaryViewModel</p>
<pre><code>@HiltViewModel
class DiaryViewModel @Inject constructor(
    private val repository: DiaryRepository
) : ViewModel() {

    ...

    private val _diaries = MutableStateFlow&lt;List&lt;Diary&gt;&gt;(emptyList())
    val diaries = _diaries.asStateFlow()

    ...

    init {
        viewModelScope.launch(Dispatchers.IO) {
            repository.getAllDao().distinctUntilChanged().collect { diaryList -&gt;
                if (diaryList.isEmpty()) {
                    Log.d(TAG, &quot;empty diary table&quot;)
                } else {
                    _diaries.value = diaryList
                }
            }
        }
    }
    ...
</code></pre><p>저번에 생성한 ViewModel을 커스텀해서 repository를 생성자에 넣어줬다.
ViewModel이 init될때 비동기로 repository에서 Database에 저장된 일기를 가져와 diaries StateFlow 변수에 입력해준다.</p>
<p>이렇게 가져온 diaries는 </p>
<pre><code>@Composable
fun diaryScreen(diaryViewModel: DiaryViewModel = hiltViewModel()) {

    ...    
    val diaries by diaryViewModel.diaries.collectAsState()
    ...
}</code></pre><p>이렇게 Compose UI내에서 최신 데이터를 유지하며 원하는 대로 UI를 만들어주면 된다.</p>
<h3 id="-느낀점">// 느낀점</h3>
<p>RoomDB 자체는 안드로이드에서 기본적으로 지원했던 만큼 레퍼런스를 찾기도 쉬웠고 Hilt와의 결합도 어렵지 않았다.</p>
<p>게시글을 작성하며 어노테이션을 하나하나 어떤 역할을 하는지 살펴보기 위해 내부 구조를 열심히 봤다.. 
Provides와 Bind등 이해하기 어려운 부분도 많았지만 (<del>사실 아직도 잘 모르겠다..</del>) 전반적으로 사용자가 이해하기 쉽게 주석이나 직관적인 네이밍을 많이 사용해줘서 좋았다!</p>
<p>다음번에는 Retrofit을 활용해보기로 하겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Hilt - compose 도입기]]></title>
            <link>https://velog.io/@312_log/Hilt-compose-%EB%8F%84%EC%9E%85%EA%B8%B0</link>
            <guid>https://velog.io/@312_log/Hilt-compose-%EB%8F%84%EC%9E%85%EA%B8%B0</guid>
            <pubDate>Sun, 14 Apr 2024 14:25:07 GMT</pubDate>
            <description><![CDATA[<p>기존에 사용하던 koin은 런타임에 의존성이 주입되어 특정 환경에서 stable하지 않거나, 디바이스의 성능에 영향을 받는다는 이슈가 있었다.
그래서 이번 프로젝트에는 compose kotlin 네이티브 개발환경에서 가장 대중적인 Hilt를 도입해보기로 했다.</p>
<h2 id="1-환경-구축">1. 환경 구축</h2>
<p>Project gradle</p>
<pre><code>buildscript {
    dependencies {
        classpath(&quot;com.google.dagger:hilt-android-gradle-plugin:2.48&quot;)
    }
}</code></pre><p>App gradle</p>
<pre><code>plugins {
    ...
    id(&quot;com.google.devtools.ksp&quot;)
    id(&quot;com.google.dagger.hilt.android&quot;)
    ...
}


dependencies {
    ...
    // Hilt
    implementation(&quot;com.google.dagger:hilt-android:2.48&quot;)
    ksp(&quot;com.google.dagger:hilt-compiler:2.48&quot;)
    ksp(&quot;com.google.dagger:hilt-android-compiler:2.48&quot;)
    ...
}</code></pre><p>Gradle.kts에 환경에 맞는 버전의 Hilt를 추가해준다.</p>
<h2 id="2-application-클래스에-hilt선언">2. Application 클래스에 Hilt선언</h2>
<p>MainApplication.class</p>
<pre><code>@HiltAndroidApp
class MainApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        ...
    }
}</code></pre><p>최상위인 애플리케이션 클래스에 @HiltAndroidApp 어노테이션을 추가해준다.
Application 클래스가 생략되는 경우가 있는데, 직접 생성하고 manifest에 선언해주면 된다.</p>
<h2 id="3activity와-viewmodel에서-의존성-주입">3.Activity와 ViewModel에서 의존성 주입</h2>
<p>MainActivity</p>
<pre><code>@AndroidEntryPoint
class MainActivity : ComponentActivity() {

    private val mainViewModel by viewModels&lt;MainViewModel&gt;()
    ...
}
</code></pre><p>본격적으로 Hilt를 활용해 의존성을 주입할 수 있는 단계다!</p>
<p>@AndroidEntryPoint 어노테이션을 선언해줘야 액티비티를 Hilt로 필요한 의존성을 자동으로 주입하고, 적절한 시점에 컴포넌트를 인스턴스화 할 수 있게끔 해준다.</p>
<p>MainViewModel</p>
<pre><code>@HiltViewModel
class MainViewModel @Inject constructor() : ViewModel() {
    ...
}</code></pre><p>ViewModel 에서는 @HiltViewModel 어노테이션을 선언해준다.
해당 어노테이션을 통해 viewModel이  HiltViewModelFactory에서 생성 &amp; 관리되고, @AndroidEntryPoint로 지정된 Activity나 Fragment에서 검색될 수 있다.</p>
<h3 id="-적용해보면서-느낀-koin과의-차이점">// 적용해보면서 느낀 koin과의 차이점..?</h3>
<p>Koin에서는 Koin내부 클래스에서 모든 Activity와 ViewModel, Manager등의 모듈을 선언해주고 멤버변수로 활용할때마다 코드를 변경해야 했는데, Hilt에서는 어노테이션을 통해 의존성을 비교적 간단하게 주입할 수 있었다.</p>
<p>성능을 제외하고 관리적 측면에서는 Koin에서는 한눈에 어떻게 주입되는지 파악이 가능했다면, Hilt에서는 쉽고 실용적인 활용이 가능했던 것 같다.
다음에는 Hilt를 활용해 compose에서 Room 내부 데이터를 가져와보겠다..!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[리코쳇 로봇]]></title>
            <link>https://velog.io/@312_log/%EB%A6%AC%EC%BD%94%EC%B3%87-%EB%A1%9C%EB%B4%87</link>
            <guid>https://velog.io/@312_log/%EB%A6%AC%EC%BD%94%EC%B3%87-%EB%A1%9C%EB%B4%87</guid>
            <pubDate>Mon, 15 Jan 2024 13:09:16 GMT</pubDate>
            <description><![CDATA[<h1 id="리코쳇-로봇---kotlin">리코쳇 로봇 - kotlin</h1>
<p>리코쳇 로봇이라는 보드게임이 있습니다.</p>
<p>이 보드게임은 격자모양 게임판 위에서 말을 움직이는 게임으로, 시작 위치에서 목표 위치까지 최소 몇 번만에 도달할 수 있는지 말하는 게임입니다.</p>
<p>이 게임에서 말의 움직임은 상, 하, 좌, 우 4방향 중 하나를 선택해서 게임판 위의 장애물이나 맨 끝에 부딪힐 때까지 미끄러져 이동하는 것을 한 번의 이동으로 칩니다.</p>
<p>다음은 보드게임판을 나타낸 예시입니다.</p>
<p>...D..R
.D.G...
....D.D
D....D.
..D....
여기서 &quot;.&quot;은 빈 공간을, &quot;R&quot;은 로봇의 처음 위치를, &quot;D&quot;는 장애물의 위치를, &quot;G&quot;는 목표지점을 나타냅니다.
위 예시에서는 &quot;R&quot; 위치에서 아래, 왼쪽, 위, 왼쪽, 아래, 오른쪽, 위 순서로 움직이면 7번 만에 &quot;G&quot; 위치에 멈춰 설 수 있으며, 이것이 최소 움직임 중 하나입니다.</p>
<p>게임판의 상태를 나타내는 문자열 배열 board가 주어졌을 때, 말이 목표위치에 도달하는데 최소 몇 번 이동해야 하는지 return 하는 solution함수를 완성하세요. 만약 목표위치에 도달할 수 없다면 -1을 return 해주세요.</p>
<h3 id="풀이-과정">풀이 과정</h3>
<p>한 방향으로 이동하기 시작하면 장애물이나 끝에 닿을때까지 이동해야 하는 좌표 이동문제다. 시작지점과 끝지점을 찾아주고 일반적으로 이동하는 방법을 while을 통해 이동할 수 없을때까지 이동해주면 된다고 이해했다.</p>
<pre><code>// 다음 이동 지점을 찾는 함수
fun findRoad(
    x: Int,
    y: Int,
    board: Array&lt;String&gt;,
    dx: Int,
    dy: Int
): Pair&lt;Int, Int&gt; {
    var currentX = x
    var currentY = y
    while (true) {
        if (currentX + dx == -1 || currentX + dx == board.size)
            return Pair(currentX, currentY)
        if (currentY + dy == -1 || currentY + dy == board[0].length)
            return Pair(currentX, currentY)
        if (board[currentX + dx][currentY + dy] == &#39;D&#39;)
            return Pair(currentX, currentY)
        currentX += dx
        currentY += dy
    }
}


fun solution(board: Array&lt;String&gt;): Int {
    var answer = Int.MAX_VALUE
    val dx = intArrayOf(0, 1, 0, -1)
    val dy = intArrayOf(-1, 0, 1, 0)
    val queueList = mutableListOf&lt;Triple&lt;Int, Int, Int&gt;&gt;()
    val visited = MutableList(board.size) { MutableList(board[0].length) { Int.MAX_VALUE } }

    // 시작 지점 찾기
    board.forEachIndexed { i, sList -&gt;
        if (sList.contains(&#39;R&#39;)) {
            queueList.add(Triple(i, sList.indexOf(&#39;R&#39;), 0))
        }
    }

    // 큐가 빌때까지 BFS
    while (queueList.isNotEmpty()) {
        val temp = queueList.removeLast()
        val current = Pair(temp.first, temp.second)
        val count = temp.third

        if (board[current.first][current.second] == &#39;G&#39;) {
            answer = Math.min(answer, count)
        }

        for (i in 0..3) {
            val toGoNode = findRoad(current.first, current.second, board, dx[i], dy[i])
            if (toGoNode != current &amp;&amp; visited[toGoNode.first][toGoNode.second] &gt; count + 1) {
                queueList.add(Triple(toGoNode.first, toGoNode.second, count + 1))
                visited[toGoNode.first][toGoNode.second] = count + 1
            }
        }
    }

    return if (answer == Int.MAX_VALUE) -1 else answer
}</code></pre><p> 처음에는 Map에 Boolean으로 방문했는지만 기록했으나, 다음 방문에 더 적은 횟수로 도달할 수 있는 경우가 있었고 Map을 Int형으로 변환해 이동 횟수가 적은 경우만 계산하고 기록하는 방법으로 해결할 수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로그래머스 - 유사 칸토어 비트열]]></title>
            <link>https://velog.io/@312_log/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%9C%A0%EC%82%AC-%EC%B9%B8%ED%86%A0%EC%96%B4-%EB%B9%84%ED%8A%B8%EC%97%B4</link>
            <guid>https://velog.io/@312_log/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%9C%A0%EC%82%AC-%EC%B9%B8%ED%86%A0%EC%96%B4-%EB%B9%84%ED%8A%B8%EC%97%B4</guid>
            <pubDate>Wed, 03 Jan 2024 02:02:53 GMT</pubDate>
            <description><![CDATA[<h1 id="유사-칸토어-비트열---kotlin">유사 칸토어 비트열 - kotlin</h1>
<p>수학에서 칸토어 집합은 0과 1 사이의 실수로 이루어진 집합으로, [0, 1]부터 시작하여 각 구간을 3등분하여 가운데 구간을 반복적으로 제외하는 방식으로 만들어집니다.</p>
<p>남아는 칸토어 집합을 조금 변형하여 유사 칸토어 비트열을 만들었습니다. 유사 칸토어 비트열은 다음과 같이 정의됩니다.</p>
<p>0 번째 유사 칸토어 비트열은 &quot;1&quot; 입니다.
n(1 ≤ n) 번째 유사 칸토어 비트열은 n - 1 번째 유사 칸토어 비트열에서의 1을 11011로 치환하고 0을 00000로 치환하여 만듭니다.
남아는 n 번째 유사 칸토어 비트열에서 특정 구간 내의 1의 개수가 몇 개인지 궁금해졌습니다.
n과 1의 개수가 몇 개인지 알고 싶은 구간을 나타내는 l, r이 주어졌을 때 그 구간 내의 1의 개수를 return 하도록 solution 함수를 완성해주세요.</p>
<h4 id="풀이-과정">풀이 과정</h4>
<p>먼저 n번만큼 과정을 반복해 수열을 만들어주고 구간내에서 1의 개수를 찾아주면 된다고 생각했다.</p>
<pre><code>class Solution {
fun solution(n: Int, l: Long, r: Long): Int {
    var current = &quot;1&quot;

    for (i in 1..n) {
        current = current.toList().joinToString(&quot;&quot;) { if (it == &#39;1&#39;) &quot;11011&quot; else &quot;00000&quot; }
    }

    return current.substring(l.toInt() - 1, r.toInt()).count { it == &#39;1&#39; }
}</code></pre><p>}</p>
<p>그러나 n이 최대 20이고 l,r이 Long타입인것을 감안했을때 다른 방법을 찾아야 했다.</p>
<h3 id="1차-개선-정확성-효율성">1차 개선 (정확성, 효율성)</h3>
<p>힌트풀이를 보고 힌트를 얻어 코드를 작성할 수 있었다..
n이 몇이 되건간에 동일한 index에는 값이 변하지 않기 때문에 n은 의미가 없었고, l과 r사이의 구간에 몇 개의 1이 존재하는지만 세면 된다.
마찬가지로 l과 r사이의 구간에도 규칙적으로 숫자가 존재하므로 재귀함수를 통해 검사함수를 만들 수 있었다.</p>
<pre><code>fun solution(n: Int, l: Long, r: Long): Int {
    var answer = 0

    fun checkNumber(number: Long): Boolean {
        if (number &lt; 5 &amp;&amp; number != 2L) return true
        if ((number - 2) % 5 == 0L) return false
        return checkNumber(number / 5)
    }

    for (i in l - 1 until r) {
        if (checkNumber(i)) answer++
    }
    return answer
}</code></pre><p>index이기 때문에 (l-1) 과 (r-1)사이의 구간에서 검사해주었다. 
&quot;11011&quot;의 구조이므로 검사 숫자에서 2를 뺀 index가 5로 나누어진다면 가운데 &quot;0&quot;인 경우 이므로 false를 반환하게 해주었다. 
그리고 첫 5까지의 index의 처리를 위해 첫 줄에 가운데 0을 제외한 나머지 숫자를 true반환 해 줄수 있었다. 
둘 다 해당하지 않는 경우 숫자가 높은 상태이므로 5로 나누어 다시 구간 처리를 해줄 수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로그래머스 - 요격 시스템]]></title>
            <link>https://velog.io/@312_log/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%9A%94%EA%B2%A9-%EC%8B%9C%EC%8A%A4%ED%85%9C</link>
            <guid>https://velog.io/@312_log/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%9A%94%EA%B2%A9-%EC%8B%9C%EC%8A%A4%ED%85%9C</guid>
            <pubDate>Tue, 26 Dec 2023 02:54:36 GMT</pubDate>
            <description><![CDATA[<h1 id="요격-시스템---kotlin">요격 시스템 - kotlin</h1>
<p>A 나라가 B 나라를 침공하였습니다. B 나라의 대부분의 전략 자원은 아이기스 군사 기지에 집중되어 있기 때문에 A 나라는 B 나라의 아이기스 군사 기지에 융단폭격을 가했습니다.
A 나라의 공격에 대항하여 아이기스 군사 기지에서는 무수히 쏟아지는 폭격 미사일들을 요격하려고 합니다. 이곳에는 백발백중을 자랑하는 요격 시스템이 있지만 운용 비용이 상당하기 때문에 미사일을 최소로 사용해서 모든 폭격 미사일을 요격하려 합니다.
A 나라와 B 나라가 싸우고 있는 이 세계는 2 차원 공간으로 이루어져 있습니다. A 나라가 발사한 폭격 미사일은 x 축에 평행한 직선 형태의 모양이며 개구간을 나타내는 정수 쌍 (s, e) 형태로 표현됩니다. B 나라는 특정 x 좌표에서 y 축에 수평이 되도록 미사일을 발사하며, 발사된 미사일은 해당 x 좌표에 걸쳐있는 모든 폭격 미사일을 관통하여 한 번에 요격할 수 있습니다. 단, 개구간 (s, e)로 표현되는 폭격 미사일은 s와 e에서 발사하는 요격 미사일로는 요격할 수 없습니다. 요격 미사일은 실수인 x 좌표에서도 발사할 수 있습니다.
각 폭격 미사일의 x 좌표 범위 목록 targets이 매개변수로 주어질 때, 모든 폭격 미사일을 요격하기 위해 필요한 요격 미사일 수의 최솟값을 return 하도록 solution 함수를 완성해 주세요.</p>
<h3 id="풀이-과정">풀이 과정</h3>
<p>처음엔 가장 많이 나온 지점을 요격하고 그 구간에 해당하는 미사일들을 삭제해서 그 개수를 세려했다. 하지만 문제 조건에 끝 지점에는 피격 판정을 두지 않았고, 그래서 요격 구간에 미사일이 존재하면 범위를 좁혀주고 존재하지 않으면 요격 구간을 추가해주기로 했다.</p>
<pre><code>fun solution(targets: Array&lt;IntArray&gt;): Int {
    val counters = mutableListOf&lt;IntRange&gt;()
    targets.sortBy { it.first() }

    B@ for (i in targets.indices) {
        val range = targets[i].first() until targets[i].last()
        for (j in counters.indices) {
            if (counters[j].intersect(range).isNotEmpty()) {
                val start = Math.max(counters[j].first, range.first)
                counters[j] =
                    start .. Math.min(counters[j].last, Math.max(start, range.last))
                continue@B
            }
        }
        counters.add(range)
    }

    return counters.count()
}</code></pre><p>정확성만 신경썼기에 효율성에서 시간초과와 메모리 초과가 있었고, 효율성을 개선해보기로 했다.</p>
<h4 id="1차-개선-효율성">1차 개선 (효율성)</h4>
<p>문제 해결을 위해 고민하던 중 범위들의 끝 값을 기준으로 정렬하고 범위 안에 있으면 끝점 갱신, 없으면 answer에 1을 더하고 다시 반복해주는 방법의 힌트를 얻었다.</p>
<pre><code>fun solution(targets: Array&lt;IntArray&gt;): Int {
    var end = 0
    var answer = 0

    targets.sortWith(compareBy { it.last() })

    for (i in targets.indices) {
        val startCurrent = targets[i].first()
        val endCurrent = targets[i].last()

        if (startCurrent &lt; end) continue
        else {
            end = endCurrent
            answer++
        }
    }

    return answer
}</code></pre><p>해당 코드를 통해 간결하고 효율성있게 해결할 수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로그래머스 - 소수 찾기]]></title>
            <link>https://velog.io/@312_log/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%86%8C%EC%88%98-%EC%B0%BE%EA%B8%B0</link>
            <guid>https://velog.io/@312_log/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%86%8C%EC%88%98-%EC%B0%BE%EA%B8%B0</guid>
            <pubDate>Thu, 21 Dec 2023 07:31:35 GMT</pubDate>
            <description><![CDATA[<h1 id="소수-찾기---kotlin">소수 찾기 - kotlin</h1>
<blockquote>
<p>한자리 숫자가 적힌 종이 조각이 흩어져있습니다. 흩어진 종이 조각을 붙여 소수를 몇 개 만들 수 있는지 알아내려 합니다.</p>
<p>각 종이 조각에 적힌 숫자가 적힌 문자열 numbers가 주어졌을 때, 종이 조각으로 만들 수 있는 소수가 몇 개인지 return 하도록 solution 함수를 완성해주세요.</p>
</blockquote>
<p>예제 #1
[1, 7]으로는 소수 [7, 17, 71]를 만들 수 있습니다.</p>
<p>String을 받고 각 숫자문자로 부분집합을 만들어 소수인지 판별하는 문제다.</p>
<h4 id="풀이-과정">풀이 과정</h4>
<p>우선 여러 과정이 필요한 만큼 헷갈리지 않게 구분하는 것이 중요하다고 생각했다.
문자열을 list<String>로 바꿔주면 부분집합을 만들어주는 재귀함수, 소수인지 판별해주는 함수를 각각 만들어 줬다.</p>
<pre><code>fun subSet(inputList: List&lt;String&gt;): List&lt;List&lt;String&gt;&gt; {
    if (inputList.isEmpty()) return listOf(emptyList())

    val element = inputList[0]
    val rest = inputList.subList(1, inputList.size)

    val subsetsWithoutElement = subSet(rest)
    val subsetsWithElement = subsetsWithoutElement.map { it + element }

    return subsetsWithoutElement + subsetsWithElement
}</code></pre><p>  재귀함수를 통해 부분집합을 생성하는 함수</p>
<pre><code>fun checkPrimeNumber(checkNumber: Int): Boolean {
    when (checkNumber) {
        0 -&gt; return false
        1 -&gt; return false
        2 -&gt; return true
        else -&gt;
            for (i in 2..checkNumber / 2 + 1) {
                if (checkNumber % i == 0) return false
            }
    }
    return true
}</code></pre><p>숫자가 소수인지 판별해주는 함수. 
효율성을 위해 변수를 두지 않고 1과 2만 따로 판별해줬다.</p>
<pre><code>fun solution(numbers: String): Int {
    var answer = 0

    subSet(numbers.map { it.toString() }).forEach {
        if (it.isEmpty()) return@forEach
        if (checkPrimeNumber(it.joinToString(&quot;&quot;).toInt())) answer++
    }

    return answer
}</code></pre><p>프로세스를 실행하는 메인 함수
그러나 입력에 &quot;17&quot;을 넣으면 (1), (7), (1, 7) 은 검색되지만 (7, 1)이 검색되지 않았다.
다른 방법을 통해 모든 경우의 수를 검색하는 방법을 생각해봤다.</p>
<h4 id="1차-수정-정확성">1차 수정 (정확성)</h4>
<p>set을 활용해 모든 경우의 부분집합을 생성해줬다.</p>
<pre><code>lateinit var numberSet: MutableSet&lt;Int&gt;

fun combinatorNumber(numbers: String, result: String) {
    if (result.isNotEmpty()) numberSet.add(result.toInt())
    if (numbers.isEmpty()) return
    numbers.forEachIndexed { index, c -&gt;
        combinatorNumber(numbers.removeRange(index..index), c.plus(result))
    }
}</code></pre><p>main에서 set을 선언해주고 count를 통해 수를 파악해주었다.</p>
<pre><code>lateinit var numberSet: MutableSet&lt;Int&gt;

fun checkPrimeNumber(checkNumber: Int): Boolean {
    when (checkNumber) {
        0 -&gt; return false
        1 -&gt; return false
        2 -&gt; return true
        else -&gt;
            for (i in 2..checkNumber / 2 + 1) {
                if (checkNumber % i == 0) return false
            }
    }
    return true
}

fun combinatorNumber(numbers: String, result: String) {
    if (result.isNotEmpty()) numberSet.add(result.toInt())
    if (numbers.isEmpty()) return
    numbers.forEachIndexed { index, c -&gt;
        combinatorNumber(numbers.removeRange(index..index), c.plus(result))
    }
}

fun solution(numbers: String): Int {
    numberSet = mutableSetOf()
    combinatorNumber(numbers, &quot;&quot;)
    return numberSet.count { checkPrimeNumber(it) }
}</code></pre><p>해당 코드를 통해 해결할 수 있었다.</p>
]]></description>
        </item>
    </channel>
</rss>