<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://cannexx.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://cannexx.github.io/" rel="alternate" type="text/html" /><updated>2026-04-14T04:22:09+00:00</updated><id>https://cannexx.github.io/feed.xml</id><title type="html"> </title><subtitle>Write an awesome description for your new site here. You can edit this line in _config.yml. It will appear in your document head meta (for Google search results) and in your feed.xml site description.</subtitle><author><name>Canna</name></author><entry><title type="html">데이터를 본다는 건, 무엇을 본다는 걸까?</title><link href="https://cannexx.github.io/data/what-are-we-looking-at/" rel="alternate" type="text/html" title="데이터를 본다는 건, 무엇을 본다는 걸까?" /><published>2026-04-14T00:00:00+00:00</published><updated>2026-04-14T00:00:00+00:00</updated><id>https://cannexx.github.io/data/what-are-we-looking-at</id><content type="html" xml:base="https://cannexx.github.io/data/what-are-we-looking-at/"><![CDATA[<p><br /><br /></p>
<h2 id="1-상황">1. 상황</h2>
<p>데이터를 아예 보지 않았던 건 아니다. 로그도 찍었고, 지표도 확인했다.<br />
구독자 수나 결제 수 같은 숫자들을 대시보드를 통해 보고 있었다.<br />
그런데 그걸 가지고 무엇을 해야 할지는 잘 몰랐다.</p>

<p>나에게 대시보드는<br />
수익이나 서비스의 방향을 확인하는 수단이라기보다는,<br />
문제 없이 잘 돌아가고 있구나 하는 일종의 상황판에 가까웠다.</p>

<p><br /></p>

<h2 id="2-질문">2. 질문</h2>

<ul>
  <li><code class="language-plaintext highlighter-rouge">나는 왜 데이터를 보고도 아무것도 하지 못했을까?</code></li>
</ul>

<p><br /></p>

<h2 id="3-돌아보면">3. 돌아보면</h2>

<p>데이터를 그저 숫자로만 보고 있었다.<br />
그게 내 일이 아니라고 생각했고, 깊게 들여다보려고 하지도 않았다.</p>

<p>프로모션으로 구독 유입이 늘어났을 때도,<br />
‘버그나 오류가 나지 않게 신경 써야겠다’ 정도로만 생각했다.</p>

<p><br /></p>

<h2 id="4-지금-생각">4. 지금 생각</h2>

<p>KPI, DAU, MAU 같은 지표들을 많이 듣고 봐왔지만,<br />
그 수치보다 수치를 해석하고 판단하는 것이 더 중요하다는 걸 알게 되었다.</p>

<p>같은 지표라도 어떤 도메인/상황에서의 수치냐에 따라 전혀 다르게 해석될 수 있고,<br />
지표 하나가 좋아졌다 하더라도 서비스가 좋아졌다고 말하기는 어렵다.</p>

<p>데이터를 본다는 건 단순히 수치를 확인하는 게 아니라<br />
여러 데이터를 함께 보면서 나름의 기준을 정하고<br />
무엇을 개선 해야 할지 생각해보는 과정인 것 같다.</p>

<p><br /></p>

<h2 id="5-남는-질문">5. 남는 질문</h2>

<ul>
  <li><code class="language-plaintext highlighter-rouge">어떤 지표가 변하면 ‘좋아졌다’고 말할 수 있을까?</code></li>
</ul>]]></content><author><name>Canna</name></author><category term="Data" /><summary type="html"><![CDATA[1. 상황 데이터를 아예 보지 않았던 건 아니다. 로그도 찍었고, 지표도 확인했다. 구독자 수나 결제 수 같은 숫자들을 대시보드를 통해 보고 있었다. 그런데 그걸 가지고 무엇을 해야 할지는 잘 몰랐다.]]></summary></entry><entry><title type="html">개발은 했지만, 무엇을 봐야 하는지는 몰랐다</title><link href="https://cannexx.github.io/data/didnt-know-what-to-look-at/" rel="alternate" type="text/html" title="개발은 했지만, 무엇을 봐야 하는지는 몰랐다" /><published>2026-04-10T00:00:00+00:00</published><updated>2026-04-10T00:00:00+00:00</updated><id>https://cannexx.github.io/data/didnt-know-what-to-look-at</id><content type="html" xml:base="https://cannexx.github.io/data/didnt-know-what-to-look-at/"><![CDATA[<p><br /><br /></p>
<h2 id="1-상황">1. 상황</h2>
<p>처음에는 단순히 ‘만드는 것’이 재미있었다.<br />
코드를 쓰는 것도, 내가 쓴 코드가 의도대로 작동하는 것도 재미있었다.</p>

<p>개발자로 일하면서 가장 많이 마주한 상황은 아래와 같았다.</p>
<ul>
  <li>정작 개발을 했는데 유저가 사용하지 않음</li>
  <li>의사결정자의 요청으로 개발했지만, 이후 트래킹은 없음</li>
  <li>추가 기능 개발 무한 반복..</li>
</ul>

<p>당시에는 그저 어린 마음에<br />
<code class="language-plaintext highlighter-rouge">‘왜 안 쓸 건데 개발해? 내 일거리만 늘어나는데..?’</code> 라는 생각을 했었다.</p>

<p>몇 번의 서비스를 거치면서 나름 사업팀, 운영팀 찾아가<br />
왜 필요한지, 다른 방법은 없는지 물어보기도 했지만 드라마틱한 변화는 없었다.</p>

<p>늘 새로운 개발 일정은 정해져있었고 한 서비스를 진득하게 볼 시간 없이 새로운 서비스, 또 새로운 서비스로 넘어갔다.</p>

<p>그때는 몰랐지만 지금 생각하기엔<br />
<code class="language-plaintext highlighter-rouge">그래서 왜 개발할 건데?</code><br />
<code class="language-plaintext highlighter-rouge">왜 필요한 건데.. 이거 하면 유저한테 뭐가 좋은 건데..</code> 라는 생각을 했다.</p>

<p><br /></p>

<h2 id="2-질문">2. 질문</h2>

<ul>
  <li><code class="language-plaintext highlighter-rouge">개발은 했는데, 왜 결과는 중요시되지 않았을까?</code></li>
  <li><code class="language-plaintext highlighter-rouge">유저에게 좋은 기능이라는 건 어떻게 판단해야 할까?</code></li>
</ul>

<p><br /></p>

<h2 id="3-돌아보면">3. 돌아보면</h2>

<p>그때는 “왜 개발하는지”보다 “무엇을 개발할지”만 중요했다.<br />
요청이 들어오면 만드는 것이 당연한 일이었다.</p>

<p>회사 내부에서 “이게 효과가 있었는지”를 보려고 트래킹을 했다.<br />
요청대로 로그도 찍고, 데이터도 남겼지만,<br />
돌이켜보면 <code class="language-plaintext highlighter-rouge">나는 그걸 깊게 들여다보지는 않았다.</code></p>

<p>결국 가장 쉬운 선택은 ‘기능 추가’<br />
기능은 계속 늘어났지만, 서비스가 좋아졌는지는 확신하지 못했다.</p>

<p><br /></p>

<h2 id="4-지금-생각">4. 지금 생각</h2>

<p>데이터 분석을 공부하면서 느낀 건,<br />
명확한 답이 주어지는 영역이 아니라는 점이다.</p>

<p>0과 1로 딱 떨어지는 개발과 달리,<br />
같은 데이터를 보고도 해석은 달라질 수 있다.</p>

<p>‘통계적으로 유의미하다’는 말도 결국 해석의 영역에 가깝고,<br />
AARRR 역시 정답을 주기보다는,<br />
지표를 통해 방향을 어떻게 잡을 수 있는지를 보여주는 프레임이다.</p>

<p>그럼에도 불구하고 하나 분명하게 느낀 건,<br />
데이터 분석은 아래와 같은 질문들을 반복하는 과정이라는 점이었다.</p>
<ul>
  <li>고객은 무엇을 원하고 있는가</li>
  <li>어떤 흐름으로 움직이고 있는가</li>
  <li>우리가 수익을 만들기 위해서는 어디를 봐야 하는가</li>
</ul>

<p>예전보다 그 질문들을 조금 더 고민하게 됐다.</p>

<p><br /></p>

<h2 id="5-남는-질문">5. 남는 질문</h2>

<p>지금 내가 생각하는 데이터 분석가는</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">수단과 상관없이, 이해관계자가 선택할 수 있도록 필요한 정보를 제공하는 역할</code></li>
</ul>

<p>그렇다면 그 안에서<br />
나는 무엇을 기준으로 보고, 무엇을 끝까지 파고들어야 할까.</p>

<p>기획자도 아니고, 개발자도 아니고, 마케터도 아닌<br />
그 사이 어딘가에 있다는 점이 더 모호하게 느껴진다.</p>

<p><br /></p>

<h2 id="-도움을-준-책">📚 도움을 준 책</h2>

<ul>
  <li>『그로스 해킹』 - 양승화</li>
</ul>]]></content><author><name>Canna</name></author><category term="Data" /><summary type="html"><![CDATA[1. 상황 처음에는 단순히 ‘만드는 것’이 재미있었다. 코드를 쓰는 것도, 내가 쓴 코드가 의도대로 작동하는 것도 재미있었다.]]></summary></entry><entry><title type="html">cameraX의 이것 저것</title><link href="https://cannexx.github.io/android/cameraX/" rel="alternate" type="text/html" title="cameraX의 이것 저것" /><published>2022-01-18T00:00:00+00:00</published><updated>2022-01-18T00:00:00+00:00</updated><id>https://cannexx.github.io/android/cameraX</id><content type="html" xml:base="https://cannexx.github.io/android/cameraX/"><![CDATA[<p><br /><br /></p>
<h2 id="1-배경">1. 배경</h2>
<p>작년 이맘 때 쯤, 카메라를 사용하는 서비스를 운영 했었다.<br />
증명을 위해 여러장의 사진(원본)을 찍어 s3로 올려야하는데 이 부분에서 업로드 지연, 사용자의 네트워크 상태로 인한 실패 등 여러 이슈들이 있었고, 그게 그 당시의 큰 숙제였다.<br /></p>

<p>어떻게 하면 사용자의 불편함을 덜면서 사진을 전송할 수 있을까 고민하고 결과를 낼때쯤.. 프로젝트가 무산되어 결국 제대로 적용해보지 못하고 덮어두었었다.</p>

<p><br />
이번에 위와 결이 비슷한 앱을 개발 검토하게 되면서 작년에 고민했던 것들을 다시 떠올려보았다.<br />
작년과 크게 다른 것은 올려야하는 사진의 수가 두 배 이상이라는 것.<br /></p>

<p>사용자 특성상 ‘찍는 속도’, ‘업로드 속도’가 중요할 것이라고 생각했고, <br />
기존에 사용하던 Intent-startActivityForResult 구조가 아닌, cameraX를 사용하면 사진 촬영에 소요되는 시간을 줄일 수 있지 않을까 싶어 적용을 해봤다.</p>

<p><br />
-) 다른 androidX 라이브러리 처럼 단순히 가져다 쓰면 되지 않을까? 했던 예상과는 다르게,,,,<br />
3A 개념 이해부터 불친절한 camaraX document(모든 예제들이 camara2 및 camaraX 내부가 모두 camara2 기반)까지 생각지 못한 벽이 많아서 여기에 정리해 두고자 한다.</p>

<p><br /><br /></p>
<hr />
<p><br /></p>

<h2 id="2-3a-설정">2. 3A 설정</h2>
<h3 id="2-1-3a란">2-1. 3A란?</h3>
<p><br /><br /></p>

<h3 id="2-2-3a값-세팅">2-2. 3A값 세팅</h3>
<p>AWB, AF, AE 각각의 모드를 명시적으로 바꿀수 있다.<br />
바꿀 수 있는 모든 상태값은 <a href="https://source.android.google.cn/devices/camera/camera3_3Amodes?hl=ko">이 링크</a>에서 볼 수 있으며, 대부분 
빠른 자동 초점 조절을 위한 AF_MODE_CONTINUOUS_PICTURE을 많이 사용한다.<br /><br /></p>

<p>기존 camera2에서는,<br />
사진을 찍기위해 생성한 captureRequest의 Builder에 set하여 모드를 세팅하면 되었는데,</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">captureBuilder</span><span class="p">.</span><span class="k">set</span><span class="p">(</span><span class="nc">CaptureRequest</span><span class="p">.</span><span class="nc">CONTROL_AE_MODE</span><span class="p">,</span> <span class="nc">CaptureRequest</span><span class="p">.</span><span class="nc">CONTROL_AE_MODE_ON</span><span class="p">);</span>
</code></pre></div></div>

<p><br />
cameraX에서는 preview를 선언하기 전 Extender를 사용해서 미리 세팅한다.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Preview</span>
<span class="kd">val</span> <span class="py">previewBuilder</span> <span class="p">=</span> <span class="nc">Preview</span><span class="p">.</span><span class="nc">Builder</span><span class="p">()</span>

<span class="kd">val</span> <span class="py">previewExtender</span> <span class="p">=</span> <span class="nc">Camera2Interop</span><span class="p">.</span><span class="nc">Extender</span><span class="p">(</span><span class="n">previewBuilder</span><span class="p">)</span>
<span class="n">previewExtender</span><span class="p">.</span><span class="nf">setCaptureRequestOption</span><span class="p">(</span>
    <span class="nc">CaptureRequest</span><span class="p">.</span><span class="nc">CONTROL_AF_MODE</span><span class="p">,</span> <span class="nc">CaptureRequest</span><span class="p">.</span><span class="nc">CONTROL_AF_MODE_CONTINUOUS_PICTURE</span>
<span class="p">)</span>

<span class="kd">val</span> <span class="py">preview</span> <span class="p">=</span> <span class="n">previewBuilder</span><span class="p">.</span><span class="nf">setTargetAspectRatio</span><span class="p">(</span><span class="n">screenAspectRatio</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">setTargetRotation</span><span class="p">(</span><span class="n">rotation</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">build</span><span class="p">()</span>
</code></pre></div></div>

<p><br /><br />
또한, Extender에 setSessionCaptureCallback 콜백함수를 달아서, <br />
리턴되는 <a href="https://developer.android.com/reference/android/hardware/camera2/TotalCaptureResult">TotalCaptureResult</a>를 통해 3A 상태를 확인할 수 있다.<br /><br />
프로젝트에서는 촬영 가능 상태값을 정의해두고 리턴값에 따라 촬영 버튼의 클릭 가능 여부(enable)를 바꾸어주었다.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">previewExtender</span><span class="p">.</span><span class="nf">setSessionCaptureCallback</span><span class="p">(</span><span class="kd">object</span> <span class="err">: </span><span class="nc">CameraCaptureSession</span><span class="p">.</span><span class="nc">CaptureCallback</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">override</span> <span class="k">fun</span> <span class="nf">onCaptureCompleted</span><span class="p">(</span>
        <span class="n">session</span><span class="p">:</span> <span class="nc">CameraCaptureSession</span><span class="p">,</span> <span class="n">request</span><span class="p">:</span> <span class="nc">CaptureRequest</span><span class="p">,</span> <span class="n">result</span><span class="p">:</span> <span class="nc">TotalCaptureResult</span>
    <span class="p">)</span> <span class="p">{</span>
        <span class="k">super</span><span class="p">.</span><span class="nf">onCaptureCompleted</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="n">result</span><span class="p">)</span>

        <span class="kd">val</span> <span class="py">afState</span> <span class="p">=</span> <span class="n">result</span><span class="p">.</span><span class="k">get</span><span class="p">(</span><span class="nc">CaptureResult</span><span class="p">.</span><span class="nc">CONTROL_AF_STATE</span><span class="p">)</span>
        <span class="kd">val</span> <span class="py">aeState</span> <span class="p">=</span> <span class="n">result</span><span class="p">.</span><span class="k">get</span><span class="p">(</span><span class="nc">CaptureResult</span><span class="p">.</span><span class="nc">CONTROL_AE_STATE</span><span class="p">)</span>
        <span class="kd">val</span> <span class="py">awbState</span> <span class="p">=</span> <span class="n">result</span><span class="p">.</span><span class="k">get</span><span class="p">(</span><span class="nc">CaptureResult</span><span class="p">.</span><span class="nc">CONTROL_AWB_STATE</span><span class="p">)</span>
        <span class="kd">val</span> <span class="py">lensState</span> <span class="p">=</span> <span class="n">result</span><span class="p">.</span><span class="k">get</span><span class="p">(</span><span class="nc">CaptureResult</span><span class="p">.</span><span class="nc">LENS_STATE</span><span class="p">)</span>
        <span class="nc">Log</span><span class="p">.</span><span class="nf">d</span><span class="p">(</span>
            <span class="s">"TEST"</span><span class="p">,</span>
            <span class="s">"afState : $afState, awbState : $awbState, aeState : $aeState, lensState : $lensState"</span>
        <span class="p">)</span>

        <span class="kd">val</span> <span class="py">value</span> <span class="p">=</span> <span class="n">afState</span> <span class="p">==</span> <span class="nc">CaptureResult</span><span class="p">.</span><span class="nc">CONTROL_AF_STATE_PASSIVE_FOCUSED</span>
                <span class="p">&amp;&amp;</span> <span class="n">awbState</span> <span class="p">==</span> <span class="nc">CaptureRequest</span><span class="p">.</span><span class="nc">CONTROL_AWB_STATE_CONVERGED</span>
                <span class="p">&amp;&amp;</span> <span class="n">lensState</span> <span class="p">==</span> <span class="nc">CaptureRequest</span><span class="p">.</span><span class="nc">LENS_STATE_STATIONARY</span>

        <span class="k">if</span><span class="p">(</span><span class="n">value</span><span class="p">){</span>
            <span class="c1">//shutter on</span>
        <span class="p">}</span><span class="k">else</span><span class="p">{</span>
            
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">})</span>
</code></pre></div></div>

<p><br /><br /></p>
<hr />
<p><br /></p>

<h2 id="3-수동-포커스-설정">3. 수동 포커스 설정</h2>
<p>기본 카메라를 보면, 자동으로 포커스를 잡다가 화면을 터치할 시 해당 위치 포커스가 된다.<br />
이후 3-5초 후, 다시 자동 포커스로 돌아가는 것을 확인할 수 있다.<br /><br /></p>

<p>주의할 점</p>
<ul>
  <li>위 2-1에서 CONTROL_AF_MODE로 CONTROL_AF_MODE_CONTINUOUS_PICTURE를 설정해두면 아래 코드가 작동하지 않는다.</li>
  <li>CONTROL_AF_MODE를 건들지 않거나 기본 모드로 설정해 두어야 작동한다.
<br /><br /></li>
</ul>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">activityMainBinding</span><span class="p">.</span><span class="n">viewFinder</span><span class="p">.</span><span class="nf">setOnTouchListener</span> <span class="p">{</span> <span class="n">_</span><span class="p">,</span> <span class="n">event</span> <span class="p">-&gt;</span>
    <span class="n">scaleGestureDetector</span><span class="p">.</span><span class="nf">onTouchEvent</span><span class="p">(</span><span class="n">event</span><span class="p">)</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">event</span><span class="p">.</span><span class="n">action</span> <span class="p">==</span> <span class="nc">MotionEvent</span><span class="p">.</span><span class="nc">ACTION_DOWN</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">handler</span><span class="p">.</span><span class="nf">removeCallbacksAndMessages</span><span class="p">(</span><span class="k">null</span><span class="p">)</span>

        <span class="kd">val</span> <span class="py">factory</span> <span class="p">=</span> <span class="n">activityMainBinding</span><span class="p">.</span><span class="n">viewFinder</span><span class="p">.</span><span class="n">meteringPointFactory</span>
        <span class="kd">val</span> <span class="py">point</span> <span class="p">=</span> <span class="n">factory</span><span class="p">.</span><span class="nf">createPoint</span><span class="p">(</span><span class="n">event</span><span class="p">.</span><span class="n">x</span><span class="p">,</span> <span class="n">event</span><span class="p">.</span><span class="n">y</span><span class="p">)</span>
        <span class="kd">val</span> <span class="py">action</span> <span class="p">=</span> <span class="nc">FocusMeteringAction</span><span class="p">.</span><span class="nc">Builder</span><span class="p">(</span><span class="n">point</span><span class="p">,</span> <span class="nc">FocusMeteringAction</span><span class="p">.</span><span class="nc">FLAG_AF</span><span class="p">)</span>
            <span class="p">.</span><span class="nf">setAutoCancelDuration</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="nc">TimeUnit</span><span class="p">.</span><span class="nc">SECONDS</span><span class="p">)</span>
            <span class="p">.</span><span class="nf">build</span><span class="p">()</span>

        <span class="cm">/*
        * ref
        * https://groups.google.com/a/android.com/g/camerax-developers/c/h0Q4Al8TXmc
        *
        * AF의 기본 설정은 계속해서 초점을 잡는 'CONTROL_AF_MODE_CONTINUOUS_PICTURE'로 되어있고,
        * startFocusAndMetering() 이 실행되면 위에서 선언한대로
        * '3초'간 'CONTROL_AF_MODE_AUTO'가 되어 클릭한 부분에 수동으로 초점을 맞출 수 있게된다.
        * 3초가 지나면 이전 상태인 'CONTROL_AF_MODE_CONTINUOUS_PICTURE'로 다시 돌아간다.
        */</span>
        <span class="n">camera</span><span class="p">.</span><span class="n">cameraControl</span><span class="p">.</span><span class="nf">startFocusAndMetering</span><span class="p">(</span><span class="n">action</span><span class="p">)</span>
        <span class="n">cameraUiContainerBinding</span><span class="o">?.</span><span class="n">ivFocusCircle</span><span class="o">?.</span><span class="nf">apply</span> <span class="p">{</span>
            <span class="n">visibility</span> <span class="p">=</span> <span class="nc">View</span><span class="p">.</span><span class="nc">VISIBLE</span>
            <span class="n">x</span> <span class="p">=</span> <span class="n">event</span><span class="p">.</span><span class="n">x</span>
            <span class="n">y</span> <span class="p">=</span> <span class="n">event</span><span class="p">.</span><span class="n">y</span>
        <span class="p">}</span>

        <span class="n">handler</span><span class="p">.</span><span class="nf">postDelayed</span><span class="p">({</span>
            <span class="n">cameraUiContainerBinding</span><span class="o">?.</span><span class="n">ivFocusCircle</span><span class="o">?.</span><span class="n">visibility</span> <span class="p">=</span> <span class="nc">View</span><span class="p">.</span><span class="nc">INVISIBLE</span>
        <span class="p">},</span> <span class="mi">3000</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="k">return</span><span class="nd">@setOnTouchListener</span> <span class="k">true</span>
<span class="p">}</span>
</code></pre></div></div>

<p>FocusMeteringAction를 이용하여 터치한 좌표에 포커스를 잡고,<br />
setAutoCancelDuration를 설정해 주어 3초 후 기본상태로 돌아가도록 한다.<br />
또한, ivFocusCircle 이미지뷰를 만들어서 해당 위치를 명시적으로 보이게 해준다.</p>

<p><br /></p>
<hr />
<p><br /></p>

<h2 id="3-코드">3. 코드</h2>
<p>전체 코드는 <a href="https://github.com/jwl-97/camera-samples/tree/main/CameraXBasic/app/src/main/java/com/android/example/cameraxbasic">여기서</a> 볼 수 있다.
<br /><br /></p>]]></content><author><name>Canna</name></author><category term="Android" /><summary type="html"><![CDATA[1. 배경 작년 이맘 때 쯤, 카메라를 사용하는 서비스를 운영 했었다. 증명을 위해 여러장의 사진(원본)을 찍어 s3로 올려야하는데 이 부분에서 업로드 지연, 사용자의 네트워크 상태로 인한 실패 등 여러 이슈들이 있었고, 그게 그 당시의 큰 숙제였다.]]></summary></entry><entry><title type="html">okt 형태소 분석기 API, JAR 비교 및 단어사전(dictionary) 추가 방법</title><link href="https://cannexx.github.io/android/okt-link-test/" rel="alternate" type="text/html" title="okt 형태소 분석기 API, JAR 비교 및 단어사전(dictionary) 추가 방법" /><published>2021-07-30T00:00:00+00:00</published><updated>2021-07-30T00:00:00+00:00</updated><id>https://cannexx.github.io/android/okt-link-test</id><content type="html" xml:base="https://cannexx.github.io/android/okt-link-test/"><![CDATA[<p><br /><br /></p>
<h2 id="1-배경">1. 배경</h2>
<p>모종의 이유로 안드로이드 앱 자체에서 특정 텍스트를 파싱해서 서버로 보내줘야 하는 일이 생겼다. <br />
그로인해 자연어 처리 도입을 고민하다가, 형태소 분석 후 명사를 가져오는 과정만 도입해도 좋을 것 같아서 도입하게 되었다.<br /></p>

<p>한글이 영어권처럼 띄어쓰기로만 명확하지 않고 의존부사 등 여러가지 케이스를 다 고려하기엔 비효율적이라고 생각해서,<br />
한글 형태소 분석기(okt, kkma, komoran..)를 찾았고, <br />
소요시간과 분석력을 비교해서 최종적으로 <a href="https://github.com/open-korean-text/open-korean-text">okt</a>(구 twitter)로 결정했다.<br /><br /></p>

<p>대부분의 형태소 분석 및 자연어 처리는 python 친화적..(KoNLPy👍) 이라서 <br />
안드로이드 상(kotlin)에서 연동했을 때 겪었던 어려웠던 점을 적어놓고자 한다.
<br /><br /></p>
<hr />
<p><br /></p>

<h2 id="2-okt-연동방법">2. okt 연동방법</h2>
<h3 id="2-1-api">2-1. API</h3>
<ul>
  <li><a href="https://github.com/open-korean-text/open-korean-text">공식 okt 사이트</a>에 들어가면 가장 먼저 API 연동이 보인다.<br />
가장 간편해보이고 속도도 가장 빠른 방법이다.<br /><br />
<img src="https://user-images.githubusercontent.com/59545680/127616308-f0d0d0c6-088d-4b15-aaf4-1b0d64ce41d0.png" width="65%" align="left" />
<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /></li>
</ul>

<h3 id="2-2-maven">2-2. maven</h3>
<ul>
  <li>지원 언어에 kotlin은 커녕 java도 없어서 불가<br /><br /></li>
</ul>

<h3 id="2-3-직접-jar-만들어서-사용">2-3. 직접 JAR 만들어서 사용</h3>
<ul>
  <li>maven을 이용한 방법은 불친절해보이기도 하고, 이것저것 할게 많아보인다.<br /><br />
<img src="https://user-images.githubusercontent.com/59545680/127620269-7277dbb4-9628-4620-8fb5-4af3196f2857.png" width="52%" align="left" />
<br /><br /><br /><br /><br /><br /><br /><br /></li>
</ul>

<h4 id="-먼저-제일-간단한-api를-통해-원하는-결과를-내보도록-하자">👉 <span>먼저 제일 간단한 API를 통해 원하는 결과를 내보도록 하자.</span></h4>
<p><br /></p>
<hr />
<p><br /></p>

<h2 id="3-okt-api">3. okt API</h2>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. 방울 방울 보라색 물 풍선 한가득
2. 워라밸을 지키기엔 일정이 너무 바빠
</code></pre></div></div>
<p>위에 주어진 요청 텍스트가 있다. <br />
여기서 <code class="language-plaintext highlighter-rouge">보라색 물풍선</code>과 <code class="language-plaintext highlighter-rouge">워라밸</code>을 골라내고 싶다.<br />
<br /><br /></p>

<p>🔽 결과 <br /></p>
<blockquote>
  <p>코드는 아래 7번 링크에서 볼 수 있다.<br /></p>
</blockquote>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>"방울(Noun: 0, 2)",
"방울 방울(Noun: 0, 5)",
"방울 방울 보라색(Noun: 0, 9)",
"방울 방울 보라색 물(Noun: 0, 11)",
"방울 방울 보라색 물 풍선(Noun: 0, 14)", /// 제일 비슷
"보라색(Noun: 6, 3)",
"풍선(Noun: 12, 2)"
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>"일정(Noun: 10, 2)"
</code></pre></div></div>
<p><br /><br />
<span>
🤔 음.. <code class="language-plaintext highlighter-rouge">보라색 물풍선</code>은 어찌저찌 가공해서 쓰면 되긴되겠지만, 원하는 바가 아니다. <br />
설령 가공한다 하더라고 모든 케이스에서 정확하게 받아올 수 있다는 보장이 없다.<br />
그리고 <code class="language-plaintext highlighter-rouge">워라밸</code>은 아예 받아오지 못한다.
</span>
<br /><br /></p>
<hr />
<p><br /></p>

<h2 id="4-dictionary의-존재-확인">4. dictionary의 존재 확인</h2>
<p>자연어 처리의 전처리 방식은 아래와 같다.                     <a href="https://paul-hyun.github.io/nlp-tutorial-01-01-sequence-prediction/">관련문서</a></p>

<blockquote>
  <ol>
    <li>토큰화</li>
    <li>정답데이터 입력</li>
    <li>사전(dictionary)) 생성</li>
    <li>학습</li>
  </ol>
</blockquote>

<p><br />
띄어쓰기, 혹은 구문단위로 토큰화를 진행하고 사전에 단어를 계속 추가하고 학습시키면서 정확도를 높이는 방법이다.<br /> 
하지만, 나는 만들어진 형태소 분석기(okt)를 사용하다 보니 정해져 있는 사전을 이용한 데이터만 받을 수 있었고,<br />
okt 공식 사이트에서 따로 정의되어 있는 사전 추가 방법은 없었다.<br /><br /></p>

<p>그러다, 사전에 원하는 단어를 추가 할 수 있다는 내용의 <a href="https://wikidocs.net/92961">한 문서</a>(KoNLPy)를 보게 되었고,
<img src="https://user-images.githubusercontent.com/59545680/127625151-e99a9183-cdb6-4af1-b686-e74419c17c4e.png" width="75%" align="left" />
<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br />
*참고)<br /> okt는 구 twitter다.<br /><br /></p>

<p>KoNLPy 라이브러리에서 가능하다면 분명히 따로 추가할 수 있는 방법이 있을 거라 생각했고,<br />
KoNLPy github issue에서 <a href="https://github.com/konlpy/konlpy/issues/249">사전 추가 방법</a>을 찾았다.<br /><br />
<br /></p>

<p>이를 적용하기 위해서 아래의 과정으로 코드를 변경했다.</p>
<blockquote>
  <ol>
    <li>API -&gt; JAR 로 연동 방법 변경</li>
    <li>dictionary 추가 후 JAR 빌드해서 적용</li>
  </ol>
</blockquote>

<p><br /> 
👉 <span>일단, JAR로 변경하여 결과를 확인해보자 </span></p>

<p><br /></p>
<hr />
<p><br /></p>

<h2 id="5-jar-생성--적용">5. JAR 생성 &amp; 적용</h2>
<h3 id="5-1-jar-생성-방법">5-1. JAR 생성 방법</h3>
<ol>
  <li>
    <p>해당 프로젝트 클론<br />
git clone https://github.com/open-korean-text/open-korean-text.git <br /></p>
  </li>
  <li>환경세팅(mac 기준)<br />
    <blockquote>
      <p>환경세팅에서 애를 좀 먹어서 버전까지 남겨놓는다. <br />
특히 scala 최신 버전은 2.13.@ 인데 해당 프로젝트에서 돌아가기 위해 2.12.@를 설치해야한다.<br /></p>
    </blockquote>

    <p>jdk(8)   [1.8.0_301]          -&gt;    oracle 설치 <br />
scala     [2.12.14]            -&gt;      brew install scala@2.12 <br />
mvn      [3.8.1]                -&gt;       brew install mvn <br />
<br /></p>
  </li>
  <li>mvn compile (run)</li>
  <li>mvn package (JAR 생성)
<br /><br /><br /></li>
</ol>

<h3 id="5-2-jar-적용-방법-in-android-studio">5-2. JAR 적용 방법 in Android Studio</h3>
<ol>
  <li>/Users/ljw/getNotiMessageApp/app/libs 안에 위에서 생성한 jar 파일 삽입</li>
  <li>
    <p>jar 파일 오른쪽 클릭 후 Add as Library 클릭</p>
  </li>
  <li>gradle에 아래 코드 추가 <br />
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>implementation'com.twitter.penguin:korean-text:4.4.4'
implementation 'org.scala-lang:scala-library:2.12.4'
</code></pre></div>    </div>
  </li>
  <li>OktTextExample 에 적용되어있는 형식대로 함수 사용</li>
</ol>

<p><br /><br /></p>

<p>🔽 결과 <br /></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>방울
방울 방울
방울 방울 보라색
방울 보라색
방울 방울 보라색 물
방울 보라색 물
보라색 물
방울 방울 보라색 물 풍선
방울 보라색 물 풍선
//// 보라색 물 풍선 ////
물 풍선
보라색
풍선
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>일정
</code></pre></div></div>

<p><br />
<span>
🤔 이제 <code class="language-plaintext highlighter-rouge">보라색 물풍선</code>은 완벽하게 얻어올 수 있다. 그러나 여전히 <code class="language-plaintext highlighter-rouge">워라밸</code>은 받아오지 못한다.
사전에 단어를 추가해보자.
</span></p>

<p><br /></p>
<hr />
<p><br /></p>

<h2 id="6-dictionary-추가">6. dictionary 추가</h2>
<ol>
  <li>
    <p>아까 클론했던 open-korean-text 파일에 들어간다.<br />
cd open-korean-text</p>
  </li>
  <li>
    <p>src/main/resources/org/openkoreantext/processor/util/noun/wikipedia_title_nouns.txt<br />
에서 원하는 명사를 추가해준다. 여기서는 <code class="language-plaintext highlighter-rouge">워라밸</code>을 추가했다.<br />
<img src="https://user-images.githubusercontent.com/59545680/127668929-09069975-811e-42df-a3e3-69afee01e965.png" width="40%" align="left" />
<br /><br /><br /><br /><br /><br /></p>
  </li>
  <li>
    <p>src/main/scala/org/openkoreantext/processor/tools/DeduplicateAndSortDictionaries.scala<br />
사전 정리 파일(인덱싱 처리)을 실행한다.</p>
  </li>
  <li>
    <p>위에서 진행했던 JAR 생성 &amp; 적용을 해준다.</p>
  </li>
</ol>

<p><br /><br />
🔽 결과 <br /></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>워라밸
일정
</code></pre></div></div>

<p><span>
👏 <code class="language-plaintext highlighter-rouge">워라밸</code> 등장 👏
</span></p>

<p><br /></p>
<hr />
<p><br /></p>

<h2 id="7-코드">7. 코드</h2>
<p>전체 코드는 <a href="https://github.com/jwl-97/oktTestApp">여기서</a> 볼 수 있다.
<br /><br /></p>]]></content><author><name>Canna</name></author><category term="Android" /><summary type="html"><![CDATA[1. 배경 모종의 이유로 안드로이드 앱 자체에서 특정 텍스트를 파싱해서 서버로 보내줘야 하는 일이 생겼다. 그로인해 자연어 처리 도입을 고민하다가, 형태소 분석 후 명사를 가져오는 과정만 도입해도 좋을 것 같아서 도입하게 되었다.]]></summary></entry><entry><title type="html">FamousSayingApp</title><link href="https://cannexx.github.io/android/famous-saying-app/" rel="alternate" type="text/html" title="FamousSayingApp" /><published>2021-05-15T00:00:00+00:00</published><updated>2021-05-15T00:00:00+00:00</updated><id>https://cannexx.github.io/android/famous-saying-app</id><content type="html" xml:base="https://cannexx.github.io/android/famous-saying-app/"><![CDATA[<p><br /><br />
이번엔 명언 앱이다. 데이터(명언, 이름)를 서버를 통하거나 앱 자체 하드 코딩이 아닌, <br />
AWS Remote Config를 통해 받아올 수 있고, 또 변수에 따라서 보여지게 설정할 수 있다. <br /><br />
여러모로 알아두면 실제 프로젝트에서도 <br />간단한 공지사항이던지 버튼 Visible 처리 등등을 쉽게 적용할 수 있을 것 같아 정리해두고 가려고 한다.</p>

<p><br />
🔽 앱 동작
<img src="https://user-images.githubusercontent.com/59545680/118363491-80481f00-b5cf-11eb-9351-35f61196ff82.gif" width="32%" /></p>

<p><br /><br /></p>

<h2 id="1-remote-config">1. Remote Config</h2>
<p><img src="https://user-images.githubusercontent.com/59545680/118363727-c2be2b80-b5d0-11eb-810b-3859365beb18.png" width="90%" /></p>

<p><br />
매개변수 추가 버튼을 통해 간단하게 추가하고,<br />
<code class="language-plaintext highlighter-rouge">Firebase.remoteConfig.getString("quotes")</code>를 통해 가져올 수 있다.<br />
다만, 맹신해서는 안되는 게, 데이터를 읽어오는데 시간이 생각보다 걸리고<br />
외부로 노출해서는 안되는 데이터는 사용하지 않는 것이 좋을 것 같다.</p>

<p><br /><br /></p>

<h2 id="2-pageradapter-무한-스크롤-처리">2. PagerAdapter 무한 스크롤 처리</h2>
<p>대부분의 adapter를 쓸 때, <code class="language-plaintext highlighter-rouge">override fun getItemCount() = items.size</code>로 사용한다.<br />
이 방법은 item의 갯수만큼 리스트를 보여주게 하는데, <br />
명언앱 같은 경우는 계속 오른쪽 또는 왼쪽으로 스크롤이 되도록 처리해야한다. 따라서 아래와 같이 처리한다.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">override</span> <span class="k">fun</span> <span class="nf">onBindViewHolder</span><span class="p">(</span><span class="n">holder</span><span class="p">:</span> <span class="nc">QuoteViewHolder</span><span class="p">,</span> <span class="n">position</span><span class="p">:</span> <span class="nc">Int</span><span class="p">)</span> <span class="p">{</span>
      <span class="kd">val</span> <span class="py">actualPosition</span> <span class="p">=</span> <span class="n">position</span> <span class="p">%</span> <span class="n">quotes</span><span class="p">.</span><span class="n">size</span> <span class="c1">//무한 스크롤 속임</span>
      <span class="n">holder</span><span class="p">.</span><span class="nf">bind</span><span class="p">(</span><span class="n">quotes</span><span class="p">[</span><span class="n">actualPosition</span><span class="p">],</span> <span class="n">isNameRevealed</span><span class="p">)</span>
  <span class="p">}</span>
  
<span class="k">override</span> <span class="k">fun</span> <span class="nf">getItemCount</span><span class="p">()</span> <span class="p">=</span> <span class="nc">Int</span><span class="p">.</span><span class="nc">MAX_VALUE</span> <span class="c1">//개수를 최대치로 </span>
</code></pre></div></div>

<p><br />
위 코드만 추가하면, 제일 처음 앱 진입시 왼쪽으로 스크롤 되지 않는 문제점이 있다.<br />
따라서 이를 해결하기 위해 adapter 세팅시 아래와 같이 첫 item의 index을 중간에 두게 설정해준다.<br /></p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">viewPager</span><span class="p">.</span><span class="nf">setCurrentItem</span><span class="p">(</span><span class="n">adapter</span><span class="p">.</span><span class="n">itemCount</span> <span class="p">/</span> <span class="mi">2</span><span class="p">,</span> <span class="k">false</span><span class="p">)</span>
</code></pre></div></div>

<p><br /><br /></p>
<h2 id="3-코드">3. 코드</h2>
<p>전체 코드는 <a href="https://github.com/jwl-97/goToBasic/tree/main/7_famousSayingApp">여기서</a> 볼 수 있다.
<br /><br /></p>]]></content><author><name>Canna</name></author><category term="Android" /><summary type="html"><![CDATA[이번엔 명언 앱이다. 데이터(명언, 이름)를 서버를 통하거나 앱 자체 하드 코딩이 아닌, AWS Remote Config를 통해 받아올 수 있고, 또 변수에 따라서 보여지게 설정할 수 있다. 여러모로 알아두면 실제 프로젝트에서도 간단한 공지사항이던지 버튼 Visible 처리 등등을 쉽게 적용할 수 있을 것 같아 정리해두고 가려고 한다.]]></summary></entry><entry><title type="html">CalculatorApp</title><link href="https://cannexx.github.io/android/calculator-app/" rel="alternate" type="text/html" title="CalculatorApp" /><published>2021-03-28T00:00:00+00:00</published><updated>2021-03-28T00:00:00+00:00</updated><id>https://cannexx.github.io/android/calculator-app</id><content type="html" xml:base="https://cannexx.github.io/android/calculator-app/"><![CDATA[<p><br /><br /></p>
<h2 id="1-배경">1. 배경</h2>
<p>이번엔 계산기 앱이다. TableLayout을 사용하고, Room으로 이력을 저장하는 등 간단한 형식의 앱이었지만,<br />
아래 사진의 빨간색으로 표시된 계산 이력 레이아웃에서 구현 방식이 새로워서 정리해 두고 가려고 한다.</p>

<p><br />
🔽 앱 동작 사진
<img src="/assets/images/calculator-app/calculatorApp1.jpg" alt="img" /> <img src="/assets/images/calculator-app/calculatorApp2.jpg" alt="img" /></p>

<p><br /><br /></p>

<h2 id="2-layoutinflater">2. LayoutInflater</h2>
<p>나였다면, RecyclerView를 써서 해당 뷰를 표현했을 것같다. 그러나, 여기서는 좀 더 간단하게 표현했다.<br />
ScrollView안에 LinearLayout을 추가하고, <br />
Inflater로 새로운 뷰를 만들어서 값을 대입한 후 addView 함수로 붙여주었다.<br /></p>

<p>무작정 Recyclerview가 아니라 이런식으로 간단하게 표현할 수 있다는게 신기했다.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;</span><span class="nc">ScrollView</span>
    <span class="n">android</span><span class="p">:</span><span class="n">layout_width</span><span class="p">=</span><span class="s">"0dp"</span>
    <span class="n">android</span><span class="p">:</span><span class="n">layout_height</span><span class="p">=</span><span class="s">"0dp"</span>
    <span class="n">android</span><span class="p">:</span><span class="n">layout_margin</span><span class="p">=</span><span class="s">"10dp"</span>
    <span class="n">app</span><span class="p">:</span><span class="n">layout_constraintBottom_toTopOf</span><span class="p">=</span><span class="s">"@id/btn_history_clear"</span>
    <span class="n">app</span><span class="p">:</span><span class="n">layout_constraintEnd_toEndOf</span><span class="p">=</span><span class="s">"parent"</span>
    <span class="n">app</span><span class="p">:</span><span class="n">layout_constraintStart_toStartOf</span><span class="p">=</span><span class="s">"parent"</span>
    <span class="n">app</span><span class="p">:</span><span class="n">layout_constraintTop_toBottomOf</span><span class="p">=</span><span class="s">"@id/btn_history_close"</span><span class="p">&gt;</span>

    <span class="p">&lt;</span><span class="nc">LinearLayout</span>
        <span class="n">android</span><span class="p">:</span><span class="n">id</span><span class="p">=</span><span class="s">"@+id/ll_history"</span>
        <span class="n">android</span><span class="p">:</span><span class="n">layout_width</span><span class="p">=</span><span class="s">"match_parent"</span>
        <span class="n">android</span><span class="p">:</span><span class="n">layout_height</span><span class="p">=</span><span class="s">"wrap_content"</span>
        <span class="n">android</span><span class="p">:</span><span class="n">orientation</span><span class="p">=</span><span class="s">"vertical"</span> <span class="p">/&gt;</span>

<span class="p">&lt;/</span><span class="nc">ScrollView</span><span class="p">&gt;</span>
</code></pre></div></div>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">Thread</span><span class="p">(</span><span class="nc">Runnable</span> <span class="p">{</span>
    <span class="n">db</span><span class="p">.</span><span class="nf">historyDao</span><span class="p">().</span><span class="nf">getAll</span><span class="p">().</span><span class="nf">reversed</span><span class="p">().</span><span class="nf">forEach</span> <span class="p">{</span> <span class="c1">//DB에서 계산 이력 데이터 불러오기</span>
        <span class="nf">runOnUiThread</span> <span class="p">{</span>
            <span class="kd">val</span> <span class="py">historyView</span> <span class="p">=</span> <span class="nc">LayoutInflater</span><span class="p">.</span><span class="nf">from</span><span class="p">(</span><span class="k">this</span><span class="p">).</span><span class="nf">inflate</span><span class="p">(</span><span class="nc">R</span><span class="p">.</span><span class="n">layout</span><span class="p">.</span><span class="n">history_row</span><span class="p">,</span> <span class="k">null</span><span class="p">,</span> <span class="k">false</span><span class="p">)</span>
            <span class="n">historyView</span><span class="p">.</span><span class="n">findViewById</span><span class="p">&lt;</span><span class="nc">TextView</span><span class="p">&gt;(</span><span class="nc">R</span><span class="p">.</span><span class="n">id</span><span class="p">.</span><span class="n">tv_expression</span><span class="p">).</span><span class="n">text</span> <span class="p">=</span> <span class="n">it</span><span class="p">.</span><span class="n">expression</span>
            <span class="n">historyView</span><span class="p">.</span><span class="n">findViewById</span><span class="p">&lt;</span><span class="nc">TextView</span><span class="p">&gt;(</span><span class="nc">R</span><span class="p">.</span><span class="n">id</span><span class="p">.</span><span class="n">tv_result</span><span class="p">).</span><span class="n">text</span> <span class="p">=</span> <span class="s">" ${it.result}"</span> <span class="c1">//값 대입</span>

            <span class="n">historyLinearLayout</span><span class="p">.</span><span class="nf">addView</span><span class="p">(</span><span class="n">historyView</span><span class="p">)</span> <span class="c1">// view 붙여넣기</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}).</span><span class="nf">start</span><span class="p">()</span>
</code></pre></div></div>
<p><br />
+) 2020.04.01 추가<br />
우리 앱에 입급정보를 단순히 보여주기만 하는 화면이 있었는데,<br />
LayoutInflater을 적용하니 recyclerview adapter나 다른 여러 코드 없이 훨씬 단순하게 바꿀 수 있었다.<br />
다만, 클릭리스너나 세세한 코드들이 필요한 경우는 적합하지 않을 것 같다.</p>

<p><br /><br /></p>
<h2 id="3-코드">3. 코드</h2>
<p>전체 코드는 <a href="https://github.com/jwl-97/goToBasic/tree/main/3_calculatorApp">여기서</a> 볼 수 있다.
<br /><br /></p>]]></content><author><name>Canna</name></author><category term="Android" /><summary type="html"><![CDATA[1. 배경 이번엔 계산기 앱이다. TableLayout을 사용하고, Room으로 이력을 저장하는 등 간단한 형식의 앱이었지만, 아래 사진의 빨간색으로 표시된 계산 이력 레이아웃에서 구현 방식이 새로워서 정리해 두고 가려고 한다.]]></summary></entry><entry><title type="html">LottoApp</title><link href="https://cannexx.github.io/android/lotto-app/" rel="alternate" type="text/html" title="LottoApp" /><published>2021-03-12T00:00:00+00:00</published><updated>2021-03-12T00:00:00+00:00</updated><id>https://cannexx.github.io/android/lotto-app</id><content type="html" xml:base="https://cannexx.github.io/android/lotto-app/"><![CDATA[<p><br /><br /></p>
<h2 id="1-배경">1. 배경</h2>
<p>요새 같은 코드만 보고 있자니 이게 맞게 하고 있는건가 라는 생각이 들때쯤, 인터넷 강의 광고를 하나 보게 되었다.  <br />
java로 구현된거나 겉핡기식이 아니라 쉽게는 레이아웃부터 클린아키텍쳐, koin, cameraX까지 커리큘럼이 나름 괜찮았다.<br />
다만, 순차적으로 나오는거라서 내가 원하는거 들을 때 까지는 몇 달을 기다려야했지만, 이틀전 첫 강의가 오픈되어 가벼운 마음으로 보기 시작했다.<br /><br />
기초와 첫강의는 어느정도 건너뛰고 두번째 강의를 들을 때, 아 역시 다른 사람 코드를 많이 봐야 하는구나 라고 생각이 들었다.<br />
새롭게 느껴졌던 코드를 몇 개 정리해 두고 가려고 한다.</p>

<p><br />
🔽 앱 동작 사진
<img src="/assets/images/lotto-app/example.jpg" alt="img" /></p>

<p><br /><br /></p>

<h2 id="2-constraintlayout">2. ConstraintLayout</h2>
<p>나는 실무에서 대부분 RelativeLayout과 LinearLayout을 사용했다.<br />
LinearLayout을 그리는 속도가 제일 늦다고 해서 거의 RelativeLayout을 사용했었는데, 사용하다보면 <br />
같은 레이아웃을 나눠쓴다거나 일정 비율을 맞춰야하는 경우가 발생할 떄 LinearLayout을 어쩔 수 없이 쓰곤 했다.<br /></p>

<p>강의를 보다보니 ConstraintLayout를 접하게 되었고, 하나 하나 이어줘야한다는게 아직 어색하지만, 그만큼 레이아웃을 자유자재로 쓸 수 있어서 좋은 것 같다. 익숙해지면 굉장히 편해질 것 같다.<br /></p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="n">app</span><span class="p">:</span><span class="n">layout_constraintLeft_toLeftOf</span><span class="p">=</span><span class="s">"parent"</span>
    <span class="n">app</span><span class="p">:</span><span class="n">layout_constraintRight_toLeftOf</span><span class="p">=</span><span class="s">"@id/btn_reset"</span>
    <span class="n">app</span><span class="p">:</span><span class="n">layout_constraintTop_toBottomOf</span><span class="p">=</span><span class="s">"@id/numberPicker"</span>
    
    <span class="cm">/**
    각각의 constraintTop / constraintLeft / constraintRight / constraintBottom에 추가로,
    toLeftOf / toRightOf / toTopOf / toBottomOf를 붙여서 간격을 설정할 수 있다.
    */</span>
</code></pre></div></div>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="n">app</span><span class="p">:</span><span class="n">layout_constraintHorizontal_chainStyle</span><span class="p">=</span><span class="s">"packed"</span>
    <span class="cm">/**
    chainStyle을 통해 같은 가로(horizontal)를 나누어 쓸 때 각각의 항목이 붙어있을지 멀리 떨어져 있을지 선택할 수 있다.
    */</span>
</code></pre></div></div>

<p><br /><br /></p>
<h2 id="3-xml-visible">3. Xml visible</h2>
<p>화면을 구성하다보면 데이터가 보여지는 타이밍이나 항목에 따라 뷰를 gone 시켰다가 다시 visible 시키는 경우가 있다.<br />
하지만 gone을 시키면 xml에서도 안보이기 때문에 불편한 경우가 있었는데, <code class="language-plaintext highlighter-rouge">android:visibility</code>은 gone시키고,<br /> <code class="language-plaintext highlighter-rouge">tools:visibility</code>은 visible로 두면 실제 뷰에서는 안보이지만, xml상에서는 보이게 처리할 수 있다.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="n">android</span><span class="p">:</span><span class="n">visibility</span><span class="p">=</span><span class="s">"gone"</span>
 <span class="n">tools</span><span class="p">:</span><span class="n">visibility</span><span class="p">=</span><span class="s">"visible"</span> 
</code></pre></div></div>
<p><br /><br /></p>
<h2 id="4-연속되는-textview-리스트화">4. 연속되는 Textview 리스트화</h2>
<p>연속되는 TextView를 list로 담아 한 번에 초기화 하고 꺼내오는 코드가 있었다.<br />
나였으면, 일일히 초기화를 해서 코드가 훨씬 더 길어졌을 텐데 리스트화하니까 가독성도 좋아지고, <br />
visible 처리나 text를 적용해줄 때, forEach나 forEachIndexed를 사용해서 한번에 쉽게 적용할 수 있었다.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="kd">val</span> <span class="py">numberTextviewList</span><span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">TextView</span><span class="p">&gt;</span> <span class="k">by</span> <span class="nf">lazy</span> <span class="p">{</span>
    <span class="n">listOf</span><span class="p">&lt;</span><span class="nc">TextView</span><span class="p">&gt;(</span>
        <span class="n">findViewById</span><span class="p">&lt;</span><span class="nc">TextView</span><span class="p">&gt;(</span><span class="nc">R</span><span class="p">.</span><span class="n">id</span><span class="p">.</span><span class="n">tv_num1</span><span class="p">),</span>
        <span class="n">findViewById</span><span class="p">&lt;</span><span class="nc">TextView</span><span class="p">&gt;(</span><span class="nc">R</span><span class="p">.</span><span class="n">id</span><span class="p">.</span><span class="n">tv_num2</span><span class="p">),</span>
        <span class="n">findViewById</span><span class="p">&lt;</span><span class="nc">TextView</span><span class="p">&gt;(</span><span class="nc">R</span><span class="p">.</span><span class="n">id</span><span class="p">.</span><span class="n">tv_num3</span><span class="p">),</span>
        <span class="n">findViewById</span><span class="p">&lt;</span><span class="nc">TextView</span><span class="p">&gt;(</span><span class="nc">R</span><span class="p">.</span><span class="n">id</span><span class="p">.</span><span class="n">tv_num4</span><span class="p">),</span>
        <span class="n">findViewById</span><span class="p">&lt;</span><span class="nc">TextView</span><span class="p">&gt;(</span><span class="nc">R</span><span class="p">.</span><span class="n">id</span><span class="p">.</span><span class="n">tv_num5</span><span class="p">),</span>
        <span class="n">findViewById</span><span class="p">&lt;</span><span class="nc">TextView</span><span class="p">&gt;(</span><span class="nc">R</span><span class="p">.</span><span class="n">id</span><span class="p">.</span><span class="n">tv_num6</span><span class="p">)</span>
    <span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">numberTextviewList</span><span class="p">.</span><span class="nf">forEachIndexed</span> <span class="p">{</span> <span class="n">index</span><span class="p">,</span> <span class="n">textview</span> <span class="p">-&gt;</span>
    <span class="n">textview</span><span class="p">.</span><span class="n">visibility</span> <span class="p">=</span> <span class="nc">View</span><span class="p">.</span><span class="nc">VISIBLE</span>
    <span class="n">textview</span><span class="p">.</span><span class="n">text</span> <span class="p">=</span> <span class="n">result</span><span class="p">[</span><span class="n">index</span><span class="p">].</span><span class="nf">toString</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>

<p><br /><br /></p>
<h2 id="5-코드">5. 코드</h2>
<p>전체 코드는 <a href="https://github.com/jwl-97/goToBasic/tree/main/1_lottoApp">여기서</a> 볼 수 있다.
<br /><br /></p>]]></content><author><name>Canna</name></author><category term="Android" /><summary type="html"><![CDATA[1. 배경 요새 같은 코드만 보고 있자니 이게 맞게 하고 있는건가 라는 생각이 들때쯤, 인터넷 강의 광고를 하나 보게 되었다. java로 구현된거나 겉핡기식이 아니라 쉽게는 레이아웃부터 클린아키텍쳐, koin, cameraX까지 커리큘럼이 나름 괜찮았다. 다만, 순차적으로 나오는거라서 내가 원하는거 들을 때 까지는 몇 달을 기다려야했지만, 이틀전 첫 강의가 오픈되어 가벼운 마음으로 보기 시작했다. 기초와 첫강의는 어느정도 건너뛰고 두번째 강의를 들을 때, 아 역시 다른 사람 코드를 많이 봐야 하는구나 라고 생각이 들었다. 새롭게 느껴졌던 코드를 몇 개 정리해 두고 가려고 한다.]]></summary></entry><entry><title type="html">안드로이드 죽지않는 서비스(Immortal Service) 구현</title><link href="https://cannexx.github.io/android/immortal-service/" rel="alternate" type="text/html" title="안드로이드 죽지않는 서비스(Immortal Service) 구현" /><published>2021-03-01T00:00:00+00:00</published><updated>2021-03-01T00:00:00+00:00</updated><id>https://cannexx.github.io/android/immortal-service</id><content type="html" xml:base="https://cannexx.github.io/android/immortal-service/"><![CDATA[<p><br /><br /></p>
<h2 id="1-배경">1. 배경</h2>
<p>내가 담당하고 있는 앱 중 하나인 <a href="https://play.google.com/store/apps/details?id=kr.co.avara.rendrivers">수퍼드라이버</a>의 가장 중요한 기능 중 하나는 <code class="language-plaintext highlighter-rouge">위치서비스</code>다.<br />
앱 출시 초창기(20년 6월~8월) 위치 서비스는 가장 큰 이슈중에 하나였다. <br /></p>

<p>우리 앱은 드라이버의 위치를 일정 간격으로 받아 주변의 콜을 안내해주고,<br />
탁송 수행 중 드라이버의 현재위치를 발주처에게 알려주기 위해 위치서비스가 꼭 필요하다.<br /></p>

<p>단순하게 앱 속  <code class="language-plaintext highlighter-rouge">출근하기</code>로 서비스를 키고, <code class="language-plaintext highlighter-rouge">퇴근하기</code>로 서비스를 끄는데 <br />
이 <code class="language-plaintext highlighter-rouge">퇴근하기</code>를 하루에 한 번씩 하는 것이 아니라 일주일이 될지, 한 달이 될지 영영 하지 않을지.. 모르는 것이었다.<br /></p>

<p>따라서 시간이 일주일 이상 흘러감에 따라 정확하지 않은 위치 데이터가 넘어오거나 출근상태임에도 불구하고<br /> 
위치데이터가 들어오지 않는 등 여러가지 변수가 있어 위치 서비스의 안정성이 크게 떨어졌다.<br /></p>

<p>이 때, 이사님(@Ryan)의 제안으로 Immortal Service를 적용하게 되었다.<br /><br /><br /></p>

<h2 id="2-immortal-service">2. Immortal Service</h2>
<p>단순히 죽지 않는 서비스이다. <br />
메인 서비스를 A라고 한다면, 서브 서비스 B를 만들어서<br />
서로서로 감시하여 onDestroy()가 호출된다면, AlarmManager를 통해 다시 서비스를 킨다.<br /></p>

<p>따라서 서비스 A가 모종의 이유로 죽으면,<br />
AlarmManager가 호출되어 서비스B가 실행되고 다시 서비스A가 실행된다.<br />
반대로, 서비스 B가 죽는다면 서비스A를 통해 서비스B가 다시 실행된다.</p>

<p>말로써 푸니까 엄청 복잡해보이는데 코드로 보면 되게 간단하다.<br /></p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Service A</span>
<span class="kd">class</span> <span class="nc">ServiceA</span> <span class="p">:</span> <span class="nc">Service</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">override</span> <span class="k">fun</span> <span class="nf">onStartCommand</span><span class="p">()</span> <span class="p">{</span>      
        <span class="k">if</span> <span class="p">(</span><span class="cm">/**intent?.getStringExtra("type")이 B이면*/</span><span class="p">){</span>
            <span class="nf">startService</span><span class="p">(</span><span class="n">serviceB</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">onDestroy</span><span class="p">()</span> <span class="p">{</span>
        <span class="c1">// 알람 매니저 호출</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="k">fun</span> <span class="nf">callAlarmManger</span><span class="p">()</span> <span class="p">{</span>
        <span class="c1">// 알람 매니저 등록</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">class</span> <span class="nc">AlarmReceiver</span> <span class="p">:</span> <span class="nc">BroadcastReceiver</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">override</span> <span class="k">fun</span> <span class="nf">onReceive</span><span class="p">(</span><span class="n">context</span><span class="p">:</span> <span class="nc">Context</span><span class="p">,</span> <span class="n">intent</span><span class="p">:</span> <span class="nc">Intent</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span><span class="p">(</span><span class="cm">/**버전 오레오 이상이면*/</span><span class="p">){</span>
            <span class="n">intent</span><span class="p">.</span><span class="nf">putExtra</span><span class="p">(</span><span class="s">"type"</span><span class="p">,</span> <span class="s">"A"</span><span class="p">)</span>
            <span class="nf">startForegroundService</span><span class="p">(</span><span class="n">serviceB</span><span class="p">)</span>
        <span class="p">}</span><span class="k">else</span> <span class="p">{</span>
            <span class="nf">startService</span><span class="p">(</span><span class="n">serviceA</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Service B</span>
<span class="kd">class</span> <span class="nc">ServiceB</span> <span class="p">:</span> <span class="nc">Service</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">override</span> <span class="k">fun</span> <span class="nf">onStartCommand</span><span class="p">()</span> <span class="p">{</span>      
        <span class="k">if</span> <span class="p">(</span><span class="cm">/**intent?.getStringExtra("type")이 A이면*/</span><span class="p">){</span>
            <span class="nf">startService</span><span class="p">(</span><span class="n">serviceA</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">onDestroy</span><span class="p">()</span> <span class="p">{</span>
        <span class="c1">// 알람 매니저 호출</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="k">fun</span> <span class="nf">callAlarmManger</span><span class="p">()</span> <span class="p">{</span>
        <span class="c1">// 알람 매니저 등록</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">class</span> <span class="nc">AlarmReceiver</span> <span class="p">:</span> <span class="nc">BroadcastReceiver</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">override</span> <span class="k">fun</span> <span class="nf">onReceive</span><span class="p">(</span><span class="n">context</span><span class="p">:</span> <span class="nc">Context</span><span class="p">,</span> <span class="n">intent</span><span class="p">:</span> <span class="nc">Intent</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span><span class="p">(</span><span class="cm">/**버전 오레오 이상이면*/</span><span class="p">){</span>
            <span class="n">intent</span><span class="p">.</span><span class="nf">putExtra</span><span class="p">(</span><span class="s">"type"</span><span class="p">,</span> <span class="s">"B"</span><span class="p">)</span>
            <span class="nf">startForegroundService</span><span class="p">(</span><span class="n">serviceA</span><span class="p">)</span>
        <span class="p">}</span><span class="k">else</span> <span class="p">{</span>
            <span class="nf">startService</span><span class="p">(</span><span class="n">serviceB</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p><br /><br /></p>

<h2 id="3-서비스-종료">3. 서비스 종료</h2>
<p>추가로, 서비스 종료하는 건 preIsRealStop 변수를 preference에 추가하여 stopService를 할때 <br />
preIsRealStop값도 같이 변경하여 알람 매니저 호출을 하지 않으면 정상적으로 종료가 된다.
<br /></p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// MainActivity</span>
<span class="nc">PreferenceUnit</span><span class="p">.</span><span class="nf">getInstance</span><span class="p">().</span><span class="n">preIsRealStop</span> <span class="p">=</span> <span class="k">true</span> <span class="c1">//</span>
<span class="nf">stopService</span><span class="p">(</span><span class="nc">Intent</span><span class="p">(</span><span class="n">applicationContext</span><span class="p">,</span> <span class="nc">ServiceA</span><span class="o">::</span><span class="k">class</span><span class="p">.</span><span class="n">java</span><span class="p">))</span>
<span class="nf">stopService</span><span class="p">(</span><span class="nc">Intent</span><span class="p">(</span><span class="n">applicationContext</span><span class="p">,</span> <span class="nc">ServiceB</span><span class="o">::</span><span class="k">class</span><span class="p">.</span><span class="n">java</span><span class="p">))</span>
</code></pre></div></div>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// In Service</span>
<span class="k">if</span> <span class="p">(!</span><span class="nc">PreferenceUnit</span><span class="p">.</span><span class="nf">getInstance</span><span class="p">().</span><span class="n">preIsRealStop</span><span class="p">)</span> <span class="p">{</span>
    <span class="nf">callAlarmManger</span><span class="p">()</span> <span class="c1">//</span>
<span class="p">}</span>
</code></pre></div></div>
<p><br /><br /></p>
<h2 id="4-코드">4. 코드</h2>
<p>전체 코드는 <a href="https://github.com/jwl-97/Android_immortalService">여기서</a> 볼 수 있다.
<br /><br /></p>]]></content><author><name>Canna</name></author><category term="Android" /><summary type="html"><![CDATA[1. 배경 내가 담당하고 있는 앱 중 하나인 수퍼드라이버의 가장 중요한 기능 중 하나는 위치서비스다. 앱 출시 초창기(20년 6월~8월) 위치 서비스는 가장 큰 이슈중에 하나였다.]]></summary></entry></feed>