<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>dev_2dong.log</title>
        <link>https://velog.io/</link>
        <description>영차영차</description>
        <lastBuildDate>Mon, 15 Aug 2022 14:21:39 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>dev_2dong.log</title>
            <url>https://images.velog.io/images/dev_2dong/profile/e9f7481e-e0ad-4070-b369-c1ed52147ab9/1591541726810.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. dev_2dong.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dev_2dong" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Git 커밋 히스토리를 관리하는 여러가지 merge 방식 [squash and merge, rebase and merge]]]></title>
            <link>https://velog.io/@dev_2dong/Git-%EC%BB%A4%EB%B0%8B-%ED%9E%88%EC%8A%A4%ED%86%A0%EB%A6%AC-%EA%B4%80%EB%A6%AC</link>
            <guid>https://velog.io/@dev_2dong/Git-%EC%BB%A4%EB%B0%8B-%ED%9E%88%EC%8A%A4%ED%86%A0%EB%A6%AC-%EA%B4%80%EB%A6%AC</guid>
            <pubDate>Mon, 15 Aug 2022 14:21:39 GMT</pubDate>
            <description><![CDATA[<h3 id="상황">상황</h3>
<p>M회사는 신규 웹 프로젝트 개발을 시작하려고 합니다. S팀장을 포함해 총 7명의 개발자가 팀에 속하게 되었습니다. 새롭게 시작하는만큼 준비해야 할 것도 많았습니다. 그 중 하나는 협업 규칙을 잘 정해두는 것이었는데요, 이를 통해 전체 소스코드의 일관성과 통일성을 유지하고 새로운 팀원이 들어왔을 때 보다 쉽게 팀에 녹아들 수 있기를 기대할 수 있었습니다. </p>
<p>여러 규칙들을 정하다보니 Git 협업 방식을 어떻게 가져갈지에 대한 이야기가 나왔습니다. 특히, 프로젝트에 참여하는 개발자가 7명이나 되기 때문에 커밋 히스토리 관리 방식을 사전에 잘 정의할 필요가 있었습니다.</p>
<p>팀장인 S씨는 누구보다 커밋 히스토리를 잘 관리하고 싶어했는데, 특히 커밋 히스토리를 복잡하지 않고 한 눈에 볼 수 있기를 원했습니다. 함께 일하는 개발자가 많아질수록 히스토리는 복잡해지고 알아보기 힘든 구조가 되기 때문입니다. 나중에 팀이 작업한 이력을 봐야 할 상황이 생길 때 이를 매끄럽게 보기 위해서는 사전에 규칙을 잘 정해야한다고 합니다.</p>
<p>S씨는 신입사원 L씨에게 어떤 방식으로 커밋 히스토리를 관리하면 팀 입장에서 생산성을 보장할 수 있을지 생각을 해보라고 과제를 던져줬습니다.</p>
<h3 id="이전-경험">이전 경험</h3>
<p>L씨는 이전에 실제로 커밋 히스토리가 복잡해서 문제를 느꼈던 프로젝트에 참여했던 경험이 있었습니다. 해당 프로젝트에서의 커밋 기록을 다시 확인해보며, 커밋 히스토리를 복잡하게 만드는 원인을 분석할 필요가 있었습니다.</p>
<center>
<img src='https://velog.velcdn.com/images/dev_2dong/post/0c583557-998d-4104-8918-1b697f85b829/image.png' width="500"/>
</center/>

<p>눈으로 보기에도 너무 많은 브랜치들이 존재하고 있고, 브랜치간의 의존도가 너무 복잡합니다. 또한, 실제 개발 내용과 무관한 커밋들이 너무 많이 존재하고 있었습니다.</p>
<p>반대로 생각했을 때, <code>브랜치를 복잡하지 않게 관리</code>하고, <code>실제 개발 내용과 관련있는 커밋만 관리</code>할 수 있다면 develop 브랜치를 보다 깔끔하게 관리할 수 있을 것 같습니다.</p>
<p>자료를 조사하던 L씨는 git의 merge방식을 이용해 커밋 히스토리를 관리할 수 있다는 글을 보았고, 해당 부분을 더 공부해보기로 했습니다. 그 결과 merge의 3가지 방식을 알게되었고, 직접 실습해보며 각 방식이 만들어내는 그래프의 모양과 장단점을 학인할 수 있었습니다. </p>
<h3 id="전제">전제</h3>
<p>이전 팀회의에서 git flow 버전 관리 방식을 이용하기로 이미 결정 된 상태입니다. </p>
<p>우선, 팀원 모두가 <code>develop</code> 브랜치를 공유합니다. 해당 브랜치는 기능 개발이 완료된 커밋들만 남게 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/dev_2dong/post/8874546c-1224-4240-a62d-1e3653443b4f/image.png" alt=""></p>
<p>이후, 각자 기능 개발을 위해 develop의 HEAD에서 <code>feature</code> 브랜치를 딴 뒤에 개발합니다. </p>
<p><img src="https://velog.velcdn.com/images/dev_2dong/post/3b56f88e-d9a8-472a-9795-8d4812b956c0/image.png" alt=""></p>
<p>개발이 끝난 이후에는 다시 <code>develop</code> 브랜치로 자신의 작업 내용을 <code>merge</code> 한다는 공통 규칙이 정해진 상태입니다.</p>
<p><img src="https://velog.velcdn.com/images/dev_2dong/post/5b9ecaaa-3ef0-40e5-987b-46b8e53615cc/image.png" alt=""></p>
<p>위와 같은 방식을 <code>fast-forward merge</code>라고 합니다. 현재 브랜치(develop)의 HEAD를 합칠 브랜치(feature/task)의 HEAD로 이동시키는 것입니다. 하지만, 해당 방식은 feature 브랜치 개발을 하던 중 develop 브랜치에 새로운 커밋이 추가되면 적용되지 못합니다. 어떤 브랜치의 HEAD가 올바른 코드인지 알 수 없기 때문입니다. (작업 영역이 동일하다면 conflict가 나기도 합니다.)</p>
<p><img src="https://velog.velcdn.com/images/dev_2dong/post/50a71c9a-f0a9-4722-be3d-8ed136b99ae1/image.png" alt=""></p>
<p>예를 들어, 다른 팀원 누군가 feature 브랜치에서의 개발을 완료하고 develop 브랜치에 merge를 수행한 상황일 수 있습니다.</p>
<p>상황을 위와 같이 맞추어두고, L씨는 여러가지 merge 방식을 테스트 해보았습니다.</p>
<h3 id="1-merge-commit">1. merge commit</h3>
<p><img src="https://velog.velcdn.com/images/dev_2dong/post/d4267148-6a36-4555-8b3f-f1781e3239ed/image.png" alt=""></p>
<p>merge commit을 생성하는 것은 가장 일반적인 방법입니다.</p>
<p>합치고 싶은 브랜치의 커밋을 참조해 새로운 커밋을 만듭니다. 위의 경우에는, [C2+C3+C4]를 참조하는 C7 커밋이 새로 생겼습니다. 또한, develop 브랜치와 feature 브랜치가 온전하게 연결되는 것을 확인할 수 있습니다.</p>
<p>해당 방식의 특징으로는, 커밋 히스토리에 정말 많은 기록들이 남게된다는 겁니다. develop의 어느 커밋으로부터 새로운 작업을 시작했는지 알 수 있고, 각 feature 브랜치의 커밋이 그대로 남아 작업 내역을 보존할 수 있습니다. 또한, merge가 되는 시점에도 기록이 남습니다.</p>
<p>기록이 남는 것이 무조건 좋지는 않습니다. 너무 자세한 기록이 남기 때문에 히스토리 그래프가 복잡해질 가능성이 높기 때문입니다. 브랜치가 많아지거나 merge가 자주 일어난다면 그래프의 가독성이 떨어질 수 밖에 없습니다. 예를 들어, 간단한 버그수정을 위해 커밋 1~2개로 이루어진 작은 길이의 feature branch가 있을수도 있는데, 이런 branch가 develop의 히스토리에 남는 것과 거의 무조건 merge commit이 생긴다는 점은 그래프를 더럽히기에 충분한 예시라고 생각합니다.</p>
<p>따라서, merge commit을 만드는 방식을 사용하는 경우에는 S팀장의 요구사항을 전혀 반영할 수 없다고 판단했습니다. develop 브랜치에서 파생된 모든 branch가 히스토리에 남게되고, 쓸데없는 merge 커밋이 생겨 커밋 내용을 확인하기 어렵게 만들기 때문입니다.</p>
<p>추가로, merge commit은 가장 일반적인 방식이기 때문에 혼자 개발을 하는 경우에 사용하기 적합한 방식이라고 생각했습니다. 왜냐하면, 혼자 개발을 한다면 여러 기능을 나누어서 개발하는 경우가 적어서 develop 브랜치의 HEAD가 feature 브랜치의 base와 동일함이 어느정도 보장되기 때문입니다.</p>
<h3 id="2-squash-and-merge">2. squash and merge</h3>
<p><img src="https://velog.velcdn.com/images/dev_2dong/post/1194aa49-4866-4f5d-be4a-9e225fb79b6b/image.png" alt=""></p>
<p>또 다른 방식으로 <code>squash and merge</code>가 있습니다. feature 브랜치를 따서 작업했던 모든 커밋 들을 합쳐 하나의 커밋으로 만들고 develop 브랜치로 병합하는 방식입니다. </p>
<p>위의 경우에 [C2+C3+C4] 커밋이 C7로 합쳐졌으며, 특이한 점으로는 C7 커밋이 feature 브랜치를 참조하지 않는다는 점이 눈에 띕니다. 참조하지 않는다는 것은 develop 브랜치를 바라봤을 때 feature 브랜치가 보이지 않는다는 뜻입니다. 따라서 보다 깔끔하게 develop 브랜치를 관리할 수 있게 됩니다.</p>
<p>해당 방식을 사용하는 경우, 커밋을 하나의 브랜치에서 관리할 수 있어 그래프가 깔끔해진다는 장점이 있습니다. 하지만, feature 브랜치에서 작업했던 모든 커밋들을 단 하나로 합치기 때문에, 세부적으로 어떤 작업을 나누어서 했는지 알기 힘들며, 커밋 단위가 매우 커질 가능성이 존재합니다.</p>
<p>해당 방식은, 브랜치를 정말 작은 단위의 feature로 <code>잘</code> 나누었을 때 사용하면 좋을 것 같습니다. 하지만, 잘 나눈다는 것은 혹시나 잘못 나누었을 때 커밋 단위가 매우 커질 수 있기 때문에 다소 아쉬움이 남는 방식이라고 생각했습니다.</p>
<h3 id="3-rebase-and-merge">3. rebase and merge</h3>
<p>마지막으로 알아본 방식은 <code>rebase and merge</code>방식입니다. rebase라는 이름은 base를 새롭게 한다는 의미를 가지고 있음을 암시적으로 나타냅니다. 특정 브랜치의 base를 내가 원하는 브랜치의 HEAD로 옮길 때 사용합니다.</p>
<p>다시 한 번 처음 상황이 어땠는지 확인을 해보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/dev_2dong/post/3222a7cd-8b21-4831-8bae-de1ab0271a11/image.png" alt=""></p>
<p>위의 그림에서 feature 브랜치는 C1을 base로 가지고 있는데, feature의 base를 develop 브랜치의 HEAD로 옮기면 자연스럽게 두 브랜치를 합칠 수 있다는 아이디어입니다. 따라서, feature의 모든 커밋들이 develop으로 옮겨 붙기를 원합니다.</p>
<p><img src="https://velog.velcdn.com/images/dev_2dong/post/d216b790-aa4a-46fd-ad3c-18ecb25b22d9/image.png" alt=""></p>
<p>하지만 엄밀히 말하면, 커밋을 그대로 옮기는 것이 아니라 기존과 동일한 내용의 커밋을 생성해 붙이는 것입니다. <code>commit id</code>를 확인하면 이를 알 수 있는데, 기존 브랜치 히스토리들의 commit id와 새로 붙은 히스토리들의 commit id를 비교하면 다르다는 것을 확인할 수 있습니다. (상단 그림에서 새로 붙은 커밋들을 [#C2, #C3]등으로 표현한 이유입니다.) </p>
<p>이후, develop 브랜치에서 feature 브랜치를 대상으로 fast-foward merge를 하면 develop의 HEAD가 feature의 HEAD로 이동하게 됩니다. (optional: feature 브랜치를 지워주면 더 깔끔해집니다.)</p>
<p>그림을 보면 알겠지만, rebase and merge를 사용하는 경우 모든 커밋을 하나의 브랜치에서 작업한 것 처럼 보이게 할 수 있습니다. 또한, squash and merge에서는 남기지 못했던 feature 브랜치의 모든 commit 기록들을 그대로 가져다 사용할 수 있습니다.</p>
<p>하지만, 어느 시점에 어떤 기능을 구현하기 시작했는지 등의 세세한 부분을 체크하기가 어렵습니다. 이러한 부분들은 PR기록을 잘 남기는 것으로 해결할 수 있습니다. </p>
<p>rebase에서 가장 큰 문제가 있다면, 히스토리 자체를 옮기는 것이기 때문에 충돌이 나는 경우 커밋마다 충돌을 해결을 해줘야 할 수 있다는 것입니다. 다른 방식들은 merge commit이 존재하기 때문에 한 번만 처리해주면 됩니다. 이를 해결하기 위해서는 feature에서 너무 오랜시간 작업을 하면 안됩니다. 또한 feature의 단위를 너무 크게 잡아서도 안됩니다.</p>
<h3 id="결론">결론</h3>
<p>L씨는 커밋 히스토리를 잘 관리하자는 관점에서 rebase and merge 방식을 제안했고 S팀장은 이를 채택했습니다. 대신, feature 단위를 작게 가져가면서 충돌이 발생할 확률을 최대한 낮추기로 했습니다. 팀의 히스토리 그래프는 깔끔해졌고, 생산성이 높아져 프로젝트를 성공적으로 마무리 할 수 있었습니다. 그리고 L씨는 공로를 인정받아 연봉이 대폭 올랐다는 해피엔딩..!</p>
<h2 id="참고-문서">참고 문서</h2>
<ul>
<li><a href="https://www.youtube.com/watch?v=TZ4IzxhjQp4">테코톡 - rebase with conflict</a></li>
<li><a href="https://dev-gorany.tistory.com/359">rebase를 사용하는 4가지 케이스</a></li>
<li><a href="https://devowen.com/430">merge vs rebase</a></li>
<li><a href="https://evan-moon.github.io/2019/08/30/commit-history-merge-strategy/">커밋 히스토리를 예쁘게 단장하자</a></li>
<li><a href="https://meetup.toast.com/posts/122">GitHub의 Merge, Squash and Merge, Rebase and Merge 정확히 이해하기</a></li>
<li><a href="https://velog.io/@kmg2933/Git-Merge-Squash-Rebase-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0">[Git] Merge, Squash, Rebase 이해하기</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[2022 주간 회고 - 29주차]]></title>
            <link>https://velog.io/@dev_2dong/2022-%EC%A3%BC%EA%B0%84-%ED%9A%8C%EA%B3%A0-29%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@dev_2dong/2022-%EC%A3%BC%EA%B0%84-%ED%9A%8C%EA%B3%A0-29%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Sun, 24 Jul 2022 13:26:22 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>29주차 : 22.07.18 (월) ~ 22.07.24 (일)</p>
</blockquote>
<p>원래 미리디 인턴 회고록을 작성하려고 하다가, 회사 외부에서도 배운 것들이 있는데 따로 기록하기가 버거워서 주간 회고 방식으로 변경하려고 한다. (<del>지금은 주간 회고지만 언제 월간 회고가 될지모름..^^</del>)</p>
<h2 id="한-일">한 일</h2>
<ol>
<li><p>회사 서비스내에서 사용되는 용어나 기술, 프로젝트 구조를 이해해야 할 필요를 느껴 문서화 작업을 시도중이다. 나중에 새로 합류하는 팀원에게 제공할 수 있을 정도로 다듬는것이 목표다. </p>
</li>
<li><p>회사에서 주어진 이슈를 처리했다.</p>
<ul>
<li>특정 상황에서 이미지 렌더링이 안되던 이슈를 처리</li>
<li>숫자만 입력 받을 수 있는 input에서 문자가 들어갔을 경우 발생하던 예외 상황을 처리</li>
</ul>
</li>
<li><p>월요일에 CS 및 알고리즘 스터디를 했다. 나는 HTTPS와 SSL 인증에 대해 공부를 해오고 발표를 진행했다. (팀원분은 OSI 7계층에 대해 공부를 해오셨다.)</p>
</li>
<li><p>인턴 동기분이 추천해준 Interactive Developer님 강의를 듣고 Canvas API를 이용해 애니메이션을 구현해봤다. Canvas 초기 세팅 방법에 집중하면서 학습했다.</p>
</li>
<li><p>알고리즘 스터디 준비를 위해 스택/큐 관련 2문제를 풀었다.</p>
</li>
<li><p>마실랭 프로젝트에서 재료 요청 POST API를 클라이언트에서 사용 가능하도록 연결했다.
Client -&gt; API Routes (NextJS Server) -&gt; API Server 흐름으로 FormData를 전달하는 과정에서 많은 이슈가 있었고, 이를 해결했다.</p>
</li>
<li><p>리액트 스터디 준비를 위해 slido 클론 코딩 관련 repository를 만들었고, 리소스 준비 및 Layout 구조를 잡았다.</p>
</li>
</ol>
<h2 id="좋았던-점">좋았던 점</h2>
<ol>
<li><p>QA 담당분이랑 소통하면서 어떤 문제를 어떻게 해결하면 좋을지 슬랙으로 이야기를 나누어봤다. 수년간 사이드 프로젝트를 하면서 타 직군 사람들이랑 소통했던 방식대로 했었는데, 회사에서도 이런 이야기 나누는 방식은 크게 다르지 않은 것 같았다. 이런 부분으로는 고민이 앞으로도 크게 없을 것 같아 좋았다.</p>
</li>
<li><p>이슈가 주어지면 이걸 언제까지 빨리 하세요! 라는 말을 안해서 좋은건지 안좋은건지는 모르겠지만, 내가 해보고 싶은 것들을 할 수 있어서 좋은 것 같다. 이번주에는 custom hook을 직접 만들어서 사용해봤는데, 시간이 지나면 자동으로 꺼지는 경고 모달을 제작해봤다. 입사 전에는 custom hook 제작이 어렵다고 느껴져서 안만들었는데, 왜 회사오니깐 안하던걸 하려고 하는지 모르겠다.</p>
</li>
<li><p>블로그에 글 쓰는게 재밌다. 아직은 작성하는데 시간이 좀 걸리기는 하지만, 나만의 탬플릿이 만들어지고 있는 것 같고 뭔가 글들이 쌓이고 있는 것 같아서 좋다. 그리고 가끔 하트가 눌리거나 댓글 달리면 누군가에게 도움이 되고 있는 것 같아 더 좋다.</p>
</li>
<li><p>역량 부족으로 인해 2주간 성공하지 못했던 마실랭 API 붙이기 작업을 성공했다. 알고보니 서버로 넘겨주는 파라미터의 명세를 잘못 알고 있었던것이 오랜 시간동안 작업을 성공하지 못했던 이유의 절반 이상이 되는 것 같았다. 올바른 문서화의 중요성을 다시 한 번 배웠다.</p>
</li>
</ol>
<h2 id="아쉬웠던-점">아쉬웠던 점</h2>
<p>사내 개발 환경에 대한 이해가 많이 부족한 것 같다. </p>
<p>사이드 프로젝트를 하는 경우에는 개발 서버와 실 서버를 동일하게 사용하는 경우가 많은데, 회사는 그런 것들이 분리가 되어있다. 그런 부분에서 내가 생각해왔던 개발 방식과 다른 부분이 생기게 되는데, 이를 고려하지 않고 개발을 하려다가 시간을 많이 날리는 것 같다.</p>
<p>이번에 외부 관리자 페이지에서 mock 데이터를 만들어줘야 개발 환경에서 테스트 가능한 컴포넌트가 있었다. 이것도 모르고, 코드 내부에서 mock 데이터를 어떻게 넣어주는지 계속 찾다가 결국 선임분이 관리자 페이지의 존재를 알려주고 난 뒤에 문제가 해결됐다. 이렇게 거의 하루가 꼬박 지났다.</p>
<p>문제를 해결하기 위해 만들어진 코드를 뒤적이고 해결 방안을 깊이 고민하는 것은 개발자의 성장 입장에서는 분명 좋은 성향이라고 생각한다. 하지만 회사에서는 조금 다르게 접근해야 할 것 같다. 이미 갖추어진 개발 환경에 익숙치 않아 발생하는 문제는 내가 아무리 코드를 분석한다고 해도 코드 외부에서 발생하는 문제이기 때문에 해결하기 힘들 수 있음을 인지해야겠다. 선임분들한테 많이 물어보면서 사내 시스템에 대한 이해를 해보자.</p>
<h2 id="느낀-점">느낀 점</h2>
<ol>
<li><p>회사 코드를 이해하는게 쉽지 않다는 것을 느꼈다. 가장 깊이있는 모듈이 어떤 기능을 하는지 이해하기 보다는 각 패키지별로 어떤 역할을 하는건지, 그 하위 디렉토리의 역할은 무엇인지를 파악하는게 중요해보인다.</p>
</li>
<li><p>내가 생각했던 직장인으로서의 삶이랑 너무 큰 차이라고 느껴질 정도로 회사 생활이 좋다. 일단 워라밸이 좋고(사실 야근하는 회사 딱히 상관 없었는데, 막상 워라밸 지켜주는 회사 오니깐 좋긴했다. 왜 다들 워라밸소리 하는 줄 알겠더라), 함께 일하는 사람들도 친절하고, 적어도 신입한테는 경험할 것들이 많다고 느껴졌기 때문이다. 그리고 아직까지는 내가 하고 싶은 것들을 고민하면서 천천히 작업할 수 있는 환경인 것 같기도 하다. 이런 특징들은 회사 동기나 선임분들도 비슷하게 생각하고 계셔서 회사의 정체성이라고 보면 될 것 같기도..</p>
</li>
<li><p>그래서 내 자신이 나태해지지만 않으면 정말 많이 성장할 수 있을 것 같다는 생각을 했다. 퇴근하고 나서나 주말에 내가 하고싶은 공부를 하면 되니깐!</p>
</li>
<li><p>이슈가 할당되고 해결하는 플로우에 대해 이해했다. 이슈가 주어지면, 해당 이슈에서 언급된 내용만 작업하고, 디벨롭하거나 개선하고 싶은 부분이 존재한다면 새로운 이슈를 만들어서 처리하는게 좋다는 것을 알게됐다. (괜히 이슈관리 시스템을 사용하는 것이 아님! 하나의 이슈를 처리하면서 연관된 다른 것들까지 처리를 해버리면, 나중에 해당 작업이 어떤 이슈에서 처리가 된건지 찾기가 너무 힘들 것 같음)</p>
</li>
<li><p>알고리즘 너무 어렵다..</p>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CSS] user-select로 텍스트 선택 방식 결정하기]]></title>
            <link>https://velog.io/@dev_2dong/CSS-user-select%EB%A1%9C-%ED%85%8D%EC%8A%A4%ED%8A%B8-%EC%84%A0%ED%83%9D-%EB%B0%A9%EC%8B%9D-%EA%B2%B0%EC%A0%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dev_2dong/CSS-user-select%EB%A1%9C-%ED%85%8D%EC%8A%A4%ED%8A%B8-%EC%84%A0%ED%83%9D-%EB%B0%A9%EC%8B%9D-%EA%B2%B0%EC%A0%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 23 Jul 2022 13:34:29 GMT</pubDate>
            <description><![CDATA[<h2 id="user-select">user-select</h2>
<p>일반적으로 텍스트를 복사할 때, 원하는 텍스트를 드래그해서 선택하는 과정을 거친다.</p>
<p>하지만, 어떤 텍스트는 선택이 안되기를 원하는 경우가 있다. 그런 경우 <code>user-select</code> 속성을 이용하면 간단하게 해결 가능하다.</p>
<p>아래와 같은 4개의 값을 사용할 수 있다.</p>
<ul>
<li>auto</li>
<li>all</li>
<li>none</li>
<li>text</li>
</ul>
<h3 id="auto">auto</h3>
<p>default 값이다.</p>
<ul>
<li>대부분 text 방식으로 동작한다.</li>
<li><code>::before</code>와 <code>::after</code>에는 none이 적용된다.</li>
<li>부모 element의 user-select값이 all/none이면 자식도 그대로 따라간다.</li>
</ul>
<h3 id="all">all</h3>
<p>더블클릭이 아닌 <code>클릭</code>만으로도 선택된다.</p>
<h3 id="none">none</h3>
<p>선택할 수 없다. 드래그, 더블클릭 모두 동작하지 않는다.</p>
<h3 id="text">text</h3>
<p>기본적으로 default와 같다. <code>더블클릭</code>을 하거나 <code>드래그</code>하는 경우 선택된다.</p>
<h2 id="사용-방법">사용 방법</h2>
<p>브라우저 호환성을 고려해 아래와 같이 사용한다.</p>
<pre><code class="language-css">-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none; 
user-select: none;</code></pre>
<p>페이지 전체를 선택되지 않게 하려면 <code>*</code> 내부에 <code>user-select: none</code>을 넣어주면 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SSL을 이용해 HTTPS 통신하기]]></title>
            <link>https://velog.io/@dev_2dong/SSL%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-HTTPS-%ED%86%B5%EC%8B%A0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dev_2dong/SSL%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-HTTPS-%ED%86%B5%EC%8B%A0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 17 Jul 2022 16:28:22 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<ul>
<li>HTTP 프로토콜의 역할과 보안 이슈에 대해 학습한다.</li>
<li>HTTPS 프로토콜이 어떤 원리로 보안을 강화하는지 학습한다.</li>
<li>SSL (Secure Sockets Layer) 프로토콜이 어떤 역할을 하는지 학습한다.</li>
<li>인증서와 인증 기관에 대해 학습한다.</li>
</ul>
<h2 id="http와-https">HTTP와 HTTPS</h2>
<blockquote>
<p>본 포스팅에서는 HTTPS의 도입 이유 및 필요성을 설명하기 위해 HTTP에 대한 간략한 이야기만 제공한다. HTTP에 대해 포스팅도 언젠가는 하겠지..?!</p>
</blockquote>
<h3 id="http-정의">HTTP 정의</h3>
<p>HTTP(HyperText Transfer Protocol)는 인터넷에서 데이터를 주고 받을 수 있도록하는 응용계층의 프로토콜이다. 클라이언트-서버 간의 통신 구조를 만드는데, 이는 클라이언트가 서버에 <code>요청</code>을 보내면 서버가 요청에 대한 결과를 만들어서 <code>응답</code>하는 것을 의미한다. 그리고 이와 관련된 규칙을 프로토콜로 정의한것이 바로 HTTP이다.</p>
<h3 id="메시지-message">메시지 (Message)</h3>
<p>클라이언트가 서버로 요청을 보내거나, 서버가 클라이언트로 응답을 보낸다고 했는데, 그 매개체는 무엇일까? 바로 <code>메시지</code>다.</p>
<p>클라이언트가 서버로 전송하는 메시지를 <code>요청 메시지 (Request Message)</code>라고 부르며, 서버에서 응답으로 전송하는 메시지를 <code>응답 메시지 (Response Message)</code>라고 부른다.</p>
<p><img src="https://velog.velcdn.com/images/dev_2dong/post/a86ead89-b000-4c94-9432-8f0fa448249f/image.png" alt=""></p>
<p>응답 메시지의 예시를 가져왔다. 요청 메시지의 경우도 비슷한 모양새를 가진다. 여기서 말하고 싶은 것은, HTTP 메시지의 경우 <code>몇 줄의 텍스트</code>로 구성된다는 것이다.</p>
<h3 id="http의-문제점">HTTP의 문제점</h3>
<p>위에서 했던 말을 정리하면 다음과 같다. </p>
<blockquote>
<p>클라이언트와 서버는 텍스트로 이루어진 메시지를 통해 소통한다.</p>
</blockquote>
<p>단순해 보이지만, 네트워크는 생각보다 많이 복잡하다. 그리고 그에 따른 보안 문제가 분명 존재한다. 예를 들어, 두 엔드포인트간 데이터가 전달되는 과정에서 수많은 라우터를 거치게 되는데, 혹시라도 나쁜 마음을 가진 해커가 경로 어딘가에서 메시지를 몰래 본다면 어떻게 될까?</p>
<p>메시지는 &#39;몇 줄의 텍스트&#39;로 이루어져있다. 심지어 <code>사람이 이해하기 쉽게 작성</code>되어있다. 즉 누구나 읽을 수 있고 의미를 해석할 수 있다. 메시지에 주민등록번호와 같은 민감한 정보가 포함된다면 외부로부터의 탈취에 대응하기 힘들다. </p>
<h3 id="https의-도입">HTTPS의 도입</h3>
<p><code>HTTPS</code>는 <strong>HTTP over Secure Sockets Layer</strong>의 약어다.</p>
<p>기존 HTTP이 보안에 취약하다는 것을 개선하고자 SSL(Secure Sockets Layer) 기술을 추가한 것이다. SSL은 <code>암호화</code> 기반의 인터넷 보안 프로토콜이다. </p>
<h3 id="ssl">SSL</h3>
<p>SSL이 제공하는 기능은 다양하다. 웹에서 전송되는 데이터를 암호화하고, 신뢰할 수 있는 사이트임을 증명하는 &#39;인증서&#39;를 검증하기도 한다. 기반 기술은 SSL에서의 암호화 방식이므로 우선 이를 이해할 수 있어야한다.</p>
<h2 id="ssl---암호화">SSL - 암호화</h2>
<h3 id="암호화와-복호화">암호화와 복호화</h3>
<p>기존 데이터를 <code>암호화된 데이터</code>로 만드는 과정을 <code>암호화</code>라고 한다. 반대로 암호화된 데이터를 원본 데이터로 되돌리는 것을 <code>복호화</code>라고 한다. </p>
<p>암호를 만들 때, 특별한 규칙 없이 만들면 나중에 해석이 불가능하다. 그래서 <code>키</code>라고 불리는 것이 필요하다. 키를 이용해 고유의 암호를 만들 수 있으며, 만들어진 암호를 복호화 할 수도 있다. 키는 문자열을 일반적으로 사용하지만 다른 자료형으로도 사용 가능하다.</p>
<p>SSL의 암호화를 이해하기 위해 필요한 암호화 방식 몇가지를 소개한다.</p>
<h3 id="대칭키-암호화">대칭키 암호화</h3>
<blockquote>
<p>키워드 : 대칭키</p>
</blockquote>
<p>데이터를 암호화하는 키와 복호화하는 키를 동일하게 가져가는 암호화 방식이다. 이러한 동일한 키를 <code>대칭키</code>라고 부른다.</p>
<p>// 이미지</p>
<p>예를 들어, 클라이언트가 대칭키를 이용해 민감한 데이터를 암호화했다고 가정한다. 서버가 클라이언트에서 제공한 대칭키를 알고 있다면, 해당 키를 이용해 데이터를 복호화해 사용하면 된다.</p>
<p>서버와 클라이언트는 사전에 대칭키를 공유해야한다. 대칭키를 공유하는 방식에서 문제가 발생하는데, 중간에 해커가 대칭키를 낚아채간다면 데이터를 누구나 복호화해서 열람할 수 있는 보안 문제가 발생한다. 따라서, 해당 대칭키를 안전하게 전달하는 것이 중요하다.</p>
<h3 id="비대칭키-암호화">비대칭키 암호화</h3>
<blockquote>
<p>키워드 : 공개키 / 개인키</p>
</blockquote>
<p>데이터를 암호화하는 키와 복호화하는 키를 다르게 사용하는 암호화 방식이다. <code>공개키</code>와 <code>개인키</code>를 이용해 데이터의 암호화를 관리한다.</p>
<p>공개키를 이용해 암호화된 데이터는 개인키로 복호화를 할 수 있다.
반대로, 개인키를 이용해 암호화된 데이터는 공개키로 복호화를 할 수 있다.</p>
<p>// 이미지</p>
<p>서버와 클라이언트는 각각 자신만의 공개키와 개인키를 가지고 있다. 어감 그대로 <code>공개키</code>는 누구에게나 공개가 가능한 키다. 탈취되어도 큰 문제가 되지는 않는다. 하지만, <code>개인키</code>는 어딘가에 노출되면 절대 안된다. 공개키로 암호화된 데이터는 개인키를 이용해서만 복호화 할 수 있기 때문에, 개인키가 노출되는 경우 큰 보안사고가 발생할 수 있다. 그냥 자신만이 잘 간직하고 있으면 된다.</p>
<h3 id="ssl이-채택한-암호화-방식">SSL이 채택한 암호화 방식</h3>
<p>대칭키를 이용해 암호화를 하면, 암호화와 복호화가 간편하다는 장점이 있다. 대신 대칭키를 공유하는 과정에서 대칭키가 노출될 수 있기 때문에, 보안적으로 취약하다는 단점이 있다.</p>
<p>비대칭키를 이용한 암호화 방식은 개인키만 잘 숨겨두면 보안적으로 안전하다는 장점이 있다. 하지만, 복잡한 수학적 원리로 암호화가 진행되기 때문에 CPU의 리소스를 많이 잡아먹는다는 문제가 있다.</p>
<p>그래서 SSL은 이 둘의 단점을 보완하고 장점을 살리기 위해 그냥 둘 다 사용한다. 즉, 보안과 성능 이슈 모두를 고려한 방식이다.</p>
<h3 id="어떻게-이게-가능할까">어떻게 이게 가능할까?</h3>
<p>대칭키는 노출되면 위험하다. 하지만, 외부에 노출되지 않은 상태로 안전하게 공유된다면 비대칭키 암호화 방식보다 더 빠르게 암호화 및 복호화를 할 수 있다.</p>
<p>사람들은 참 똑똑하다. 대칭키를 비대칭키 암호화 방식을 이용해 공유하면, 클라이언트-서버간 대칭키를 안전하게 공유할 수 있게된다.</p>
<p>// 이미지</p>
<ol>
<li>송신자는 수신자의 공개키를 이용해 대칭키를 암호화해서 넘긴다.</li>
<li>&#39;암호화된 대칭키&#39;가 수신자쪽으로 안전하게 전달된다.</li>
<li>수신자는 전달받은 &#39;암호화된 대칭키&#39;를 자신의 개인키로 복호화한다.</li>
<li>수신자는 원래 대칭키를 잘 가지고있으면 된다.</li>
</ol>
<p>그 이후로는, 해당 대칭키를 이용해 데이터를 암호화해서 보내고, 받은 데이터를 복호화해서 사용하면 된다.</p>
<p>정리하면 다음과 같다.</p>
<blockquote>
<p>비대칭키 암호화 방식을 이용해 대칭키를 공유하고,
대칭키 암호화 방식을 이용해 실제 데이터를 송·수신하는 방식을 이용한다.</p>
</blockquote>
<h2 id="ssl---신뢰할-수-있는-사이트임을-검증하기">SSL - 신뢰할 수 있는 사이트임을 검증하기</h2>
<p>SSL에서 사용하는 암호화 방식을 이용하면, 데이터를 중간에 탈취당해도 해커가 어떤 데이터인지 알아내기 힘들기 때문에 상당히 안전하다는 것을 언급했다.</p>
<p>하지만, 애초에 내 소중한 개인정보를 &#39;악의적인 목적을 가진 사이트&#39;에 전송해버리는 경우가 생길 수 있다. (사칭 페이지나, 스팸 페이지 등 정말 다양한 케이스가 있다.) 이런 경우, 아무리 암호화가 된다한들 개인정보를 보호할 수 없게된다.</p>
<p>SSL은 암호화 방식에서 더 나아가서, 신뢰할 수 있는 사이트임을 <code>인증서</code>를 이용해 검증하는 방식까지 지원한다. 이를 통해, 사용자는 보다 더 안전하게 웹을 탐험할 수 있다.</p>
<h3 id="ca-certificate-authority">CA (Certificate Authority)</h3>
<p>CA는 인증서를 발급하고 관리하는 기관이다. 신뢰할 수 있는 사이트임을 검증하는 책임이 있다. <strong>CA는 자신만의 공개키와 개인키를 가지고 있다.</strong> 당연히 CA의 개인키가 노출되면 모든 &#39;인증서&#39;의 보안에 문제가 생기게된다.</p>
<p>보안을 조금 더 강화하기 위해, CA는 2계층 이상의 연쇄적인 구조를 가지고 있다.</p>
<ol>
<li>Root CA (최상위 기관)</li>
<li>Intermediate CA (중간 기관)</li>
</ol>
<p>하위 CA가 상위 CA에게 인증서를 요청하고, 또 상위 CA는 더 상위 CA에게 인증서를 요청하는 방식이다. 이를 <code>인증서 체인</code>이라고 한다.</p>
<h3 id="인증서">인증서</h3>
<p>서버는 자신이 신뢰할 수 있는 안전한 서버임을 모두에게 알리고 싶어한다. 이를 증명하는 문서가 필요한데, 이 때 필요한 것이 <code>인증서</code>다. 흔히 SSL 인증서라고 부르며, CA로부터 발급 받을 수 있다.</p>
<p>서버가 인증서를 발급받는 과정을 설명하면서, 인증서가 어떻게 구성되어 있는지를 설명하겠다.</p>
<ol>
<li>우선, 서버가 CA에게 인증서 발급을 요청하기 위해서 자신의 <code>도메인 주소</code>와 <code>공개키</code>를 넘겨줘야한다. </li>
<li>CA는 서버의 공개키를 서명 알고리즘을 이용해 해시하는데, 이를 <code>지문 (Fingerprint)</code>이라고 부른다. </li>
<li>이후, 만들어진 지문을 CA의 개인키를 이용해 암호화한다. 이를 <code>서명 (Signature)</code>이라고 한다.</li>
</ol>
<p>이를 통해, 인증서에 어떤 정보가 있는지 생각을 할 수 있다.</p>
<ol>
<li>지문과 서명</li>
<li>서명 알고리즘 (지문을 다시 공개키로 만들어야 하니깐)</li>
<li>인증서 발급 기관</li>
<li>서버 도메인</li>
</ol>
<h3 id="신뢰할-수-있는-사이트임을-클라이언트가-인식하는-과정">신뢰할 수 있는 사이트임을 클라이언트가 인식하는 과정</h3>
<blockquote>
<p>브라우저는 이미 검증된 CA와 CA의 공개키를 알고있다. 이를 전제로 해야한다.</p>
</blockquote>
<p>SSL 인증서를 가지고 있는 서버에 접속했을 때, 해당 SSL 인증서가 올바른 것인지, 위조 된 것인지 알 수 있는 방법을 소개한다.</p>
<p>핵심은 <code>인증서의 서명을 복호화한 값</code>과 <code>지문</code>이 일치하는지를 파악하는 것이다.</p>
<p>클라이언트가 서버에 접속하면, 서버는 클라이언트에게 자신의 인증서를 넘겨준다. 인증서에는 해당 인증서를 발급해준 CA 정보가 존재하는데, CA 리스트를 확인해 해당 CA의 공개키를 획득 후 인증서의 서명을 복호화한다. 서명을 복호화 한 값이랑 서버의 공개키를 해시한 값이 일치한다면 인증서가 위조되지 않았음을 나타내는 것이다.</p>
<p>이후에는 획득한 서버의 공개키를 이용해, 대칭키를 암호화하고 처음에 설명했던 보안 통신을 진행하는 것이다.</p>
<h2 id="생각해보기">생각해보기</h2>
<h3 id="get-요청과-post-요청의-보안">GET 요청과 POST 요청의 보안</h3>
<p>예전에 모 스타트업 면접에서 &#39;GET과 POST의 차이에 대해 설명&#39;해달라는 질문을 받은적이 있었다. </p>
<p>설명 중 GET 방식은 url에 쿼리 파라미터가 그대로 노출되므로 보안에 취약하고, POST 방식은 message body에 숨겨놓기 때문에 보안에 조금 더 유리하다고 설명했다.</p>
<p>그 때, 면접관분이 어쨌든 Network 탭으로 확인하면 GET이나 POST나 관련 데이터를 확인할 수 있는데 왜 POST가 보안에 더 유리한거죠? 라는 질문을 했었는데.. 잘 모르겠다하고 넘어갔던 기억이 있다.</p>
<p>그래서 그냥 GET이나 POST나 보안이랑은 큰 차이가 없나보다~ 하고 말았었는데, 이번에 암호화 방식에 대해 공부하다보니 생각을 조금 달리할 수 있었다.</p>
<p>SSL 방식을 이용한 HTTPS 통신 과정에서 Message가 전체적으로 암호화되기 때문에, 중간에 해당 데이터를 탈취당하더라도 해커는 &#39;암호화된 Message&#39;밖에 못본다. 암호화된 데이터는 클라이언트 사이드에서 복호화를 하며, 데이터를 전송하는 경우에도 암호화된 데이터를 보내기 때문에 GET 방식보다 보안에 안전하다는 판단이 가능한 것 같다.</p>
<h2 id="참고자료">참고자료</h2>
<ul>
<li><a href="https://www.youtube.com/watch?v=wPdH7lJ8jf0">[10분 테코톡] 🍭 다니의 HTTPS</a></li>
<li><a href="https://www.crocus.co.kr/1387#:~:text=SSL%EC%9D%B4%EB%9E%80%20%EB%B3%B4%EC%95%88%20%EC%86%8C%EC%BC%93%20%EA%B3%84%EC%B8%B5,%EC%95%94%ED%98%B8%ED%99%94%ED%95%B4%EC%84%9C%20%EB%B3%B4%EB%82%BC%20%EC%88%98%20%EC%9E%88%EB%8B%A4">HTTPS, SSL 개념</a></li>
<li><a href="https://universitytomorrow.com/22">무조건 이해 시켜드립니다. 대칭키와 비대칭키에 대하여</a></li>
<li><a href="https://developer.mozilla.org/ko/docs/Web/HTTP/Overview">MDN HTTP 개요</a></li>
<li><a href="https://babbab2.tistory.com/5?category=960153">인증서, CA, SSL 인증서를 통해 서버를 인증하는 방법</a></li>
<li><a href="https://m.blog.naver.com/alice_k106/221468341565">[Security] SSL과 인증서 구조 이해하기</a></li>
<li><a href="https://www.cloudflare.com/ko-kr/learning/ssl/what-is-ssl/">SSL이란 무엇입니까?</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[2022 미리디 여름 인턴 2주차 회고]]></title>
            <link>https://velog.io/@dev_2dong/2022-%EB%AF%B8%EB%A6%AC%EB%94%94-%EC%97%AC%EB%A6%84-%EC%9D%B8%ED%84%B4-2%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@dev_2dong/2022-%EB%AF%B8%EB%A6%AC%EB%94%94-%EC%97%AC%EB%A6%84-%EC%9D%B8%ED%84%B4-2%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sat, 16 Jul 2022 16:36:57 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>2주차 : 22.07.11 (월) ~ 22.07.15 (금)</p>
</blockquote>
<h2 id="가장-기억에-남는-일">가장 기억에 남는 일</h2>
<h3 id="사내-스터디-시작">사내 스터디 시작</h3>
<p>마음 맞는 동기들끼리 모여서 사내 스터디를 하게 되었다. 혼자 공부하는 것을 좋아하는 스타일이기는 하지만, 목표가 동일한 열정 넘치는 동료들과 함께 공부하는 것도 큰 도움이 된다는 것을 알고 있기에 오히려 좋다. 회사에서 스터디를 하는게 로망이긴 했는데.. 이렇게 갑자기 하게 될 줄은 몰랐다..ㅋㅋㅋ 어쨌든 기회가 생겼으니 두 달 동안 피해 안가도록 열심히 해봐야겠다.</p>
<p>월요일, 목요일 퇴근 시간 이후 60~90분 정도 스터디를 진행한다.
월요일에는 팀원들과 개인적으로 공부한 CS 내용을 공유한다. 또한, 내부 팀원끼리 사전에 정한 알고리즘 문제를 풀어와서 서로 피드백을 해주는 시간도 있다.
목요일에는 정해진 범위의 리액트 공식문서를 읽어온 뒤, 자신이 학습한 내용을 기반으로 예제를 만들어와 동료들에게 공유하는 시간을 가진다.</p>
<p>이번 <strong>스터디를 통해 얻고 싶은 것</strong>은</p>
<ol>
<li>꾸준하게 주 2회정도 알고리즘 문제 푸는 습관을 기를 수 있으면 좋겠다.</li>
<li>블로그에 지식을 기록하는 습관을 기를 수 있으면 좋겠다.</li>
<li>리액트에 어떤 기능들이 있는지, 사용법은 어떻게 되는건지 완벽하게 파악하고 싶다.</li>
</ol>
<p>준비하는 시간이 빡빡할 것 같다는 부분이 조금 걱정되긴 한다! 요건 진행하면서 진행 방식을 변경할 수 있으니, 일단은 현재 규칙에 맞추어 최선을 다해봐야겠다.</p>
<h3 id="실-서비스의-버그-발견">실 서비스의 버그 발견</h3>
<p>리팩토링을 하다가 실 서비스의 버그를 발견했고, 이슈를 만들어 팀원들에게 공유했다. 모바일 한정 이슈였는데, 특정 이미지가 보여야 할 자리에 이미지가 렌더링이 안되던 것이었다. 이슈를 공유한 뒤 문제를 해결하는 과정을 경험했다는 것 자체가 나에게 의미있었다.</p>
<p>왜 의미가 있었을까?</p>
<p>나는 팀원들이랑 프로젝트를 할 때 마다</p>
<ol>
<li>문제나 개선 가능성이 있는 부분을 발견하고</li>
<li>해당 상황을 좋은 표현을 사용한 언어로 알리고</li>
<li>해결방안을 혼자 혹은 같이 고민하고</li>
<li>어떻게 해결했는지 공유하는</li>
</ol>
<p>그런 상황에 놓이는 것을 좋아했다. 새로운 기능을 추가하는 것도 좋지만, 기존의 문제를 발견하고 개선하는 것은 <strong>프로그램의 완성도를 높이고</strong>, <strong>사용자에게 좋은 경험을 줄 수 있는 방식</strong>이라고 생각하기 때문에 내 몸과 머리가 이런 상황을 반기는 것 같다😇</p>
<p>그런 관점에서, 발견한 문제를 팀원들에게 알리고 고치는 경험 자체가 회사에서 처음이라 떨리고 재미있었던 것 같다. (사실 내 적성은 qa쪽에 있는지도 모르겠다..^^)</p>
<h2 id="한-일">한 일</h2>
<p>요거는 너무 구체적으로 적으면 안될거같아서 대충 적어두었다.</p>
<ul>
<li>팀 내 git flow에 대해 학습</li>
<li>첫 이슈를 할당 받고 작업 수행 (emotion 작업 관련 이슈)</li>
</ul>
<h2 id="좋았던-점">좋았던 점</h2>
<ol>
<li><p>프로그래밍 관점에서의 고민을 함께 나눌 수 있다는 점이 좋았다. 하나의 사례로, icon의 background-image를 결정해서 css 템플릿으로 만들어주는 함수명에 대한 고민이 깊었는데, 동료들에게 상황을 공유했더니 금방 깔끔하고 참심하고 멋진 이름을 만들 수 있었다.</p>
</li>
<li><p>사내 스터디를 시작하기로 했다. 월요일에는 알고리즘 / CS, 목요일에는 리액트 공식문서를 읽기로 했다. 인턴 기간동안, 회사 업무 외적으로도 스스로 성장할 수 있는 다른 방법을 찾으려고 했었는데 열정 넘치는 동기들과 스터디를 하게 되어 좋다.</p>
</li>
<li><p>운동을 시작했다! 회사 다니면서 운동 다니는 멋진 분들 많이 봐오면서, 나도 회사다니면 꼭 운동하겠다고 다짐했었다. 그랬던 다짐을 생각보다 빨리 이룰 수 있어서 좋았다.</p>
</li>
<li><p>프로젝트 내 설정되어있는 Lint로 인해 내가 작성하고 싶은 로직이 막히는 경우가 종종 있었다. 덕분에 문제 상황에 대한 해결책을 여러가지로 고민해보게 되고, 더 확실한 해결책을 찾으려고 노력하게 되는 것 같아 좋았다.</p>
</li>
<li><p>실 서비스에서 동작하는 버그를 찾았고 팀원들과 이야기를 나누어봤다. 뭔가 실제 업무와 관련된 의사소통을 아주 약간이나마 한 것 같아서 좋았다.</p>
</li>
<li><p>Git GUI를 사용하지 않고 CLI 환경에서 git을 다루기로 결정했다. 이렇게 하면 git에 대해 조금 더 이해할 수 있지 않을까 싶었다. 실제로 stash 명령을 명령어로 조작하면서 이전에 GUI 환경에서 사용했던 것 보다 더 다양한 기능을 사용해보기도 했다. </p>
</li>
<li><p>TypeScript의 제네릭을 맨날 써봐야지 하다가도 어디에서 써야할지 모르겠고 넘 어려워서 항상 넘겼다. 이번에 제네릭을 쓰면 확장성 측면에서 유리할 것 같은 util 함수가 떠올랐는데, 간단하게 공부해서 제네릭을 이용한 함수를 하나 만들어봤다. 살면서 처음으로 제네릭을 이용해봐서 기분 좋았다. </p>
</li>
</ol>
<h2 id="아쉬웠던-점">아쉬웠던 점</h2>
<ol>
<li><p>나한테 먼저 말걸어준 입사 동기가 떠나버렸다 ㅠ_ㅠ</p>
</li>
<li><p>선임분께 Git PR 날리는 상황에서의 꿀팁을 들었는데, 정확한 흐름을 잊어버렸다..ㅎㅎ; 나중에 또다시 여쭙게 될 것 같아 미리 죄송하다..</p>
</li>
<li><p>회사 코드를 만지다보니, 다른 사람들이 이해할 수 있는 코드를 작성하자는 생각이 평소보다 더 강하게 들었다. 코드를 작성하는데 시간이 많이 걸렸고, 업무 시간이 생각보다 빠르게 지나갔다. 간단한 리팩토링 이슈를 해결하는 것도 2~3일 정도 걸렸는데, 어느정도의 개선이 필요하다고 생각했다.</p>
</li>
<li><p>뭔가 하나씩 놓치는 것 같다. 이번주만 해도 책 리뷰하는거나, 서류 작성하는 것들.. 하나씩 빼먹고 일주일을 보내버렸다. 사소하지만 해야하는 것들을 포스트잇에 적어서 몰아서 하거나, 점심시간에 처리하는 등 대책을 세워야겠다.</p>
</li>
</ol>
<h2 id="느낀-점">느낀 점</h2>
<ol>
<li><p>스터디를 하기로 했지만, 퇴근하고 공부하기에는 시간이 많이 부족할 것 같다. 효율적으로 잘 해보려고 노력을 많이 해야겠다. (집은 최대한 일찍 가야겠다.)</p>
</li>
<li><p>지난주는 많이 졸렸는데, 이번주는 안그랬다. 잠을 많이 잔것도 아닌데 신기하다. 운동을 해서 그런가?</p>
</li>
<li><p>환경에 따라 사람 성격이 참 많이 변하는 것 같다. 신기하다.</p>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[알고리즘] 프로그래머스 / 베스트앨범 / Level 3]]></title>
            <link>https://velog.io/@dev_2dong/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%B2%A0%EC%8A%A4%ED%8A%B8%EC%95%A8%EB%B2%94-Level-3</link>
            <guid>https://velog.io/@dev_2dong/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%B2%A0%EC%8A%A4%ED%8A%B8%EC%95%A8%EB%B2%94-Level-3</guid>
            <pubDate>Sat, 16 Jul 2022 06:39:23 GMT</pubDate>
            <description><![CDATA[<h2 id="정보">정보</h2>
<ul>
<li>문제 : <a href="https://school.programmers.co.kr/learn/courses/30/lessons/42579">프로그래머스 - 베스트앨범</a></li>
<li>분류 : 해시</li>
<li>난이도 : Level 3</li>
</ul>
<h2 id="해결-방법-생각의-흐름">해결 방법 (생각의 흐름)</h2>
<h3 id="1-장르별-전체-재생-횟수-관리하기">1. 장르별 전체 재생 횟수 관리하기</h3>
<p>문제 조건에 &#39;많이 재생된 장르의 순서&#39;대로 값을 출력하라는 말이 있기 때문에, 전체 재생 횟수를 관리해야 한다고 생각했다.</p>
<p>입력값에는 아래와 같은 정보가 들어있다.</p>
<ul>
<li>각 노래의 장르 (<code>genres</code>)<ul>
<li><code>[&quot;classic&quot;, &quot;pop&quot;, &quot;classic&quot;, &quot;classic&quot;, &quot;pop&quot;]</code></li>
</ul>
</li>
<li>각 노래의 재생횟수 (<code>plays</code>)<ul>
<li><code>[500, 600, 150, 800, 2500]</code></li>
</ul>
</li>
</ul>
<p><code>genreToPlayCount</code> Map 자료구조를 만들어 장르의 전체 재생 횟수를 관리할 수 있다.</p>
<ul>
<li>key : 장르</li>
<li>value : 해당 장르의 전체 재생 횟수</li>
</ul>
<p>입력값이 배열이므로, 전체를 순회하여 <code>genreToPlayCount</code>의 값을 채워넣을 수 있다.</p>
<h3 id="2-장르별-포함하고-있는-노래의-정보를-저장하기">2. 장르별 포함하고 있는 노래의 정보를 저장하기</h3>
<p>장르별로 어떤 노래를 가지고있는지 저장할 필요가 있다고 생각했다. 
정보를 알고싶은 장르가 선택되고나면, 해당 장르의 노래 정보를 바로 가져다가 조작하는게 합리적이라고 생각했기 때문이다.</p>
<p>따라서, <code>genreToSongList</code> Map 자료구조를 만들었고 1번과 마찬가지로 전체 배열을 순회해 데이터를 저장해두었다.</p>
<p>여기까지 작업 코드는 아래와 같다.</p>
<pre><code class="language-js">const genreToPlayCount = new Map();
const genreToSongList = new Map();

for(let i = 0; i &lt; genres.length; i++) {
  const [genre, playCount] = [genres[i], plays[i]];
  const prevPlayCount = genreToPlayCount.get(genre) || 0;
  genreToPlayCount.set(genre, prevPlayCount + playCount);

  const prevSongList = genreToSongList.get(genre) || [];
  genreToSongList.set(genre, prevSongList.concat({ play: playCount, idx: i }));
}</code></pre>
<p>문제에서는 노래의 id를 배열의 idx로 관리하는데, 위의 Map을 만드는 과정에서 idx가 모두 깨져버리므로 genreToSongList의 노래정보에 idx도 추가해주었다.</p>
<h3 id="3-전체-재생횟수에-따라-장르를-구분하기">3. 전체 재생횟수에 따라 장르를 구분하기</h3>
<p>문제에 아래와 같은 조건이 있다.</p>
<blockquote>
<p>모든 장르는 재생된 횟수가 다릅니다.</p>
</blockquote>
<p>이 말을 곱씹어 생각해보니, 굳이 key값을 장르로하는 Map을 만들 필요가 없어보였다. 그리고 아래와 같은 플로우를 생각할 수 있었다.</p>
<ol>
<li>전체 재생 횟수에 따라 정렬을 한다.</li>
<li>정렬된 재생 횟수에 배열에서 정보를 얻고자하는 원소를 선택한다.</li>
<li>이와 매핑되는 장르를 선택을 할거다.</li>
<li>선택된 장르를 key로 <code>genreToSongList</code>에서 노래 정보를 꺼내와 추가 작업을 진행한다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/dev_2dong/post/a25e78ca-2447-4c88-8d56-970c544a4f7a/image.png" alt=""></p>
<p>이를 위해 아래와 같은 <code>totalCountToGenre</code> Map을 만들었다.</p>
<pre><code class="language-js">const totalCountToGenre = new Map();
genreToPlayCount.forEach((totalCount, genre) =&gt; {
  totalCountToGenre.set(totalCount, genre);
})</code></pre>
<p>(근데 사실 조금 마음에 안들기는 했다. 그 이유는 아래 생각이나 느낀점에 적어두었다.)</p>
<h3 id="4-어떤-장르의-노래를-가장-많이-들었을까">4. 어떤 장르의 노래를 가장 많이 들었을까?</h3>
<p>이제 출력을 준비해야한다.</p>
<p>가장 재생 횟수가 많은 장르 순으로 정렬을 해야한다.</p>
<pre><code class="language-js">Array.from(genreToPlayCount.values())
    .sort((a, b) =&gt; b - a)    // 장르의 재생 횟수가 가장 많은 것부터 내림차순 정렬</code></pre>
<p>위의 코드를 통해 가장 많이 들은 <code>장르의 재생 횟수</code>는 알 수 있지만, 어떤 장르인지는 알 수 없다.</p>
<pre><code class="language-js">const genre = totalCountToGenre.get(playCount);    // playCount : 위의 배열의 원소 중 하나</code></pre>
<p>그래서 3번 과정에서 이를 매핑해주는 Map을 만들었다. 이제 장르를 알 수 있다.</p>
<h3 id="5-가장-많이-들은-장르의-노래부터-최대-2개씩-꺼내오기">5. 가장 많이 들은 장르의 노래부터 최대 2개씩 꺼내오기</h3>
<p>장르별로 어떤 노래를 가지고 있는지는 2번에서 만든 <code>genreToSongList</code> Map을 통해 얻어올 수 있다.</p>
<p>해당 노래 리스트에서는 아래와 같은 조건대로 정렬을 수행해야한다. </p>
<ul>
<li>장르 별로 가장 많이 재생된 노래를 <strong>두 개씩</strong> 모아 베스트 앨범을 출시하려 합니다.</li>
<li>장르 내에서 <strong>많이 재생된 노래를 먼저 수록</strong>합니다.</li>
<li>장르 내에서 재생 횟수가 같은 노래 중에서는 <strong>고유 번호가 낮은 노래를 먼저 수록</strong>합니다.</li>
</ul>
<p>이는 자바스크립트의 <code>Array.prototype.sort()</code> 함수를 이용해 해결할 수 있다.</p>
<p>4번 ~ 5번 과정을 정리한 코드는 아래와 같다.</p>
<pre><code class="language-js">Array.from(genreToPlayCount.values())
  .sort((a, b) =&gt; b - a)
  .reduce((ans, playCount) =&gt; {
    const genre = totalCountToGenre.get(playCount);
    const songList = genreToSongList.get(genre);
    const sortedSongList = songList
      .sort((a, b) =&gt; a.play === b.play ? a.idx - b.idx : b.play - a.play)
      .slice(0, 2);
      ans.push(...(sortedSongList.map(item =&gt; item.idx)));
      return ans;
  }, []);</code></pre>
<h2 id="전체-코드">전체 코드</h2>
<p><a href="https://github.com/linear14/algorithm/blob/master/miridih-study/P_%EB%B2%A0%EC%8A%A4%ED%8A%B8%EC%95%A8%EB%B2%94/solution.js">깃허브 소스코드</a></p>
<pre><code class="language-js">const solution = (genres, plays) =&gt; {
    const genreToPlayCount = new Map();
    const genreToSongList = new Map();

    for(let i = 0; i &lt; genres.length; i++) {
        const [genre, playCount] = [genres[i], plays[i]];
        const prevPlayCount = genreToPlayCount.get(genre) || 0;
        genreToPlayCount.set(genre, prevPlayCount + playCount);

        const prevSongList = genreToSongList.get(genre) || [];
        genreToSongList.set(genre, prevSongList.concat({ play: playCount, idx: i }));
    }

    const totalCountToGenre = new Map();
    genreToPlayCount.forEach((totalCount, genre) =&gt; {
        totalCountToGenre.set(totalCount, genre);
    })

    return Array.from(genreToPlayCount.values())
        .sort((a, b) =&gt; b - a)
        .reduce((ans, playCount) =&gt; {
            const genre = totalCountToGenre.get(playCount);
            const songList = genreToSongList.get(genre);
            const sortedSongList = songList
                .sort((a, b) =&gt; a.play === b.play ? a.idx - b.idx : b.play - a.play)
                .slice(0, 2);
            ans.push(...(sortedSongList.map(item =&gt; item.idx)));
            return ans;
        }, []);
};</code></pre>
<h2 id="그냥-생각이나-느낀점">그냥 생각이나 느낀점</h2>
<ul>
<li>문제를 꼼꼼히 읽어야겠다. 처음에는 <code>장르</code>도 전체 재생횟수 많은 순으로 상위 2개만 선택하는 것인줄 알고 넘 복잡하게 생각해서 시간 날렸다.</li>
<li>Map과 Object의 차이점 다시 읽고 정리해두면 좋을 것 같다.</li>
<li>genreToSongList의 값을 최신화 할 때, 기존 배열에 concat을 붙여서 새로 배열을 만들었다. 사실 입력값이 크면 메모리 문제가 발생할 수 있을 것 같아서 별로 맘에 안든다. (왜 선택했냐면, 저렇게 안하니깐 코드가 넘 더러워져서..)</li>
<li><code>totalCountToGenre</code> 맵 객체를 만들었는데, <code>genreToPlayCount</code>에 값을 모두 넣은 뒤 key-value 값만 바꾸는 느낌이라.. 굳이 Map을 각각 만들어야하나? 하는 생각이 들었다. 지금 생각해보니 안만들고 기존꺼(genreToPlayCount에) 쓰면 될 것 같은데 한 번 작업해봐야겠다.</li>
<li>비슷하게 <code>genreToPlayCount</code>, <code>genreToSongList</code> 요것도 같은 key값으로 value만 다르게 관리되고 있는데 하나로 합칠수도 있을 것 같다.</li>
<li>알고리즘 어렵다.</li>
</ul>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://kellis.tistory.com/129">Map vs Object</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[2022 미리디 여름 인턴 1주차 회고]]></title>
            <link>https://velog.io/@dev_2dong/2022-%EB%AF%B8%EB%A6%AC%EB%94%94-%EC%97%AC%EB%A6%84-%EC%9D%B8%ED%84%B4-1%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@dev_2dong/2022-%EB%AF%B8%EB%A6%AC%EB%94%94-%EC%97%AC%EB%A6%84-%EC%9D%B8%ED%84%B4-1%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Mon, 11 Jul 2022 14:02:22 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>1주차 : 22.07.01 (금) ~ 22.07.08 (금)</p>
</blockquote>
<h2 id="가장-기억에-남는-일">가장 기억에 남는 일</h2>
<h3 id="첫-출근">첫 출근</h3>
<p>이번주 가장 기억남는 일은 아무래도 첫 출근이었다.</p>
<p>회사들 면접을 보러가면 항상 넓고 깨끗한 사무실을 꼭 마주했었다. 이런곳에서 일을 한다는 것이 어떤 기분일까 항상 궁금했었고 나도 저 자리에 앉아서 일을 해보고 싶었다. 미리디는 비대면 면접이었기 때문에 입사 전까지 회사 내부를 구경했던 적이 없었다. 그러니깐 더더욱 내 자리가 생긴다는 것이 기대가 되고 설렜다. 한편으로는 잘 적응할 수 있을까 긴장도 많이 했다!</p>
<p>회사 입구에 도착하니 피플팀 직원분께서 자리를 안내해주셨다. 자리에는 무려 맥북이랑 마우스 키보드, 웰컴키트랑 읽을 책 등이 있었다. 같이 입사한 인턴 동기와 인사도 하고, 앉아서 맥북 세팅하고, 간단한 출퇴근 지문등록 한 다음에, 교육도 듣고 점심도 먹고 또 교육도 들으면서 하루가 끝났다. 엄청나게 많은 것을 하거나 특별한 것을 하지는 않았지만 정신없이 하루가 흘러갔다.</p>
<h2 id="한-일">한 일</h2>
<p>이번주는 계정 권한 설정이나 신규입사자들이 꼭 해야하는 투두리스트 체크를 주로 진행하는 등의 기본적인 일들을 수행했다. 또한, 여러 동료들을 만나면서 회사와 팀 문화를 익히고 적응하려고 노력했다.</p>
<p>회사에서 지급해준 맥북에 적응하는 것도 일이었다. ctrl 대신 command를 사용한다는 것이나 한/영키가 CapsLock 위치에 있다는 것이 적응이 힘들었다. 심지어 한/영키를 눌러도 동작하지 않는 경우가 매우 빈번히 있었는데, 이는 <code>Karabiner</code>를 이용해 해결할 수 있었다.</p>
<p>터미널을 zsh로 사용해보는 것도 처음이었는데, iterm2와 powerlevel9k를 이용해 터미널을 예쁘게 꾸밀 수 있다는 사실도 알게되어서 꾸미기도 해봤다.</p>
<p>팀에서는 첫 이슈로 &#39;프로그래머의 뇌&#39;라는 책을 읽는것을 할당 받았다. 프로그래머가 정보를 효율적으로 기억하도록 돕는 방법이나, 클린 코드가 작업의 효율성에 어떤 좋은 영향을 미치는지 등 알아두면 좋을 것들을 정리한 책이었다. 이를 통해 우리팀이 어떤 코딩 스타일을 추구하는지, 팀에서 신입들에게 어떤 역량을 원하는지 약간이나마 느낄 수 있었다.</p>
<p>개발환경을 세팅하는 과정에서 vscode에서 yarn2를 사용하면서 컨피그 이슈가 발생했다. 다행히 선임 개발자분이 과거에 겪었던 동일 이슈를 문서화해서 관리했고, 해당 이슈를 이후의 개발자들이 겪지 않도록 이미 작업을 다 해두셨다. 이런 모습들을 보면서 나도 팀원들에게 도움이 되는 개발자가 되기 위해 어디에 노력을 투자해야하는지 간접적으로 알 수 있었다. </p>
<p>개인적으로는 컨피그 이슈를 해결하려고 시도하는 과정에서 yarn2의 존재에 대해 알게 되었는데, yarn2의 존재를 알게되면서 기존 npm이나 yarn이 가지고 있는 의존성 관리 방식의 문제에 대해서도 알게 되었다. (나중에 조금 더 공부하면서 포스팅해도 좋을듯!)</p>
<p>깃에 대해서도 살짝 더 공부했다. origin이 뭔지, git rebase를 하면 어떤 일이 발생하는지 등에 대해서 깊이 생각했던 적이 없었는데 역시 공부해야 할 상황이 만들어지니 꼼꼼히 공부하게 되는 것 같다.</p>
<h2 id="동료">동료</h2>
<p>생각을 곰곰히 해보다가 문득 알게됐는데, 새로운 사람을 대면으로 만나서 이야기하는게 거의 5년정도 됐다. 근래에는 대부분 비대면 활동 위주로 하기도 했으니깐.. 그 사이에 사회성이 많이 떨어지지는 않았을까 걱정이 많이 됐다😥</p>
<p>어떻게 보면 인턴 기간동안의 목표 중에서 가장 중요한건 동료들과 많은 소통을 하면서 친해지는 것일수도 있겠다는 생각이 들었다. 나에게 먼저 다가와주는 고마운 동료들도 있고, 내가 먼저 다가가서 고마워하는 동료도 있었다. 다들 좋은 관계를 위해 노력한다는 것을 느꼈고, 서로가 서로에게 좋은 동료가 되기 위해 나먼저 노력해야겠다는 생각을 하게됐다.</p>
<p>이번에 인턴 수가 상당히 많다. 미리디에는 팀이 정말 많은데, 각 팀에서 소수의 인원만 인턴으로 뽑아도 충분히 많아질 수 밖에 없다고 생각한다. 앞으로 살면서 동기가 이토록 많은 환경에 놓일 수 없을 것 같다는 생각이 들었는데, 애초에 사회에서 정말 많은 동기들과 서로 의지하면서 성장할 수 있는 환경이 만들어졌다는게 흔한 일은 아닐 것 같다. 동기들이랑 친하게 지내고 재미있게 회사생활 해야겠다는 생각이 들었다.</p>
<h2 id="좋았던-점">좋았던 점</h2>
<ol>
<li><p>팀에서 모던한 기술들을 많이 사용한다는 점이 좋았다. 기술적인 변화를 적극적으로 맞이하는 팀 처럼 느껴져서 내가 생각하는 가치와 맞았다는 점이 너무 좋았다.</p>
</li>
<li><p>회사에서 위키를 사용해 많은 것들을 기록할 수 있도록 시스템을 구축해놨다. 업무상 이슈나, 이를 해결한 과정을 기록하는 것을 좋아하는 편이라 위키가 활성화 되어있다는게 좋았다.</p>
</li>
<li><p>회사 분위기도 너무 좋았다. 팀원들이랑 같이 점심 식사를 하러 갔는데, 편하게 해주려고 신경 써주시는게 보여서 감사했다. 어서 팀에 녹아들어야겠다는 생각을 했다.</p>
</li>
<li><p>오랜만에 새로운 사람들 만나서 이야기하는게 좋았다. 먼저 용기내서 다가와준 분들에게는 당연히 너무 고맙다. 개인적으로는 평소에 모르는 사람들에게 먼저 말 거는것을 잘 못하는데, 먼저 밥먹자고 연락도 해봤다! (이런걸 자랑이라고 하는 낯가리는 성격 너무 싫다..😂)</p>
</li>
</ol>
<h2 id="아쉬웠던-점">아쉬웠던 점</h2>
<ol>
<li><p>귀찮더라도 하루하루를 매일 회고하는 습관을 들여놓고 싶었는데 그러지 못해서 아쉽다. 이제 막 개발환경 설정하기 시작했으니 남은 기간에 꼭 잘 정리해야겠다.</p>
</li>
<li><p>생각보다 퇴근 후에 개인적으로 보낼 수 있는 시간이 부족했다. 일찍 퇴근하는 날에도 집 도착하면 7시가 훌쩍 넘는데, 조금만 방심하면 바로 잘 시간이 되는게 아쉬웠다. 시간 관리하는 방법을 고민 해봐야겠다.</p>
</li>
<li><p>팀원분들이랑 더 친해지고 싶다. 팀에 녹아들어 서로 티키타카가 잘 될 수 있도록 대면/비대면으로 소통을 많이 해봐야겠다.</p>
</li>
</ol>
<h2 id="느낀-점">느낀 점</h2>
<ol>
<li><p>회사에 처음가면 정말 환영받는 느낌이 들어서 기분이 좋다! 내 사진으로 만들어진 웰컴 현수막 (부담스러울 줄 알았는데, 생각보다 괜찮았다ㅎㅎ), 깔끔하게 정리된 자리, 하루종일 돌아다니며 도움 주시는 감사한 피플팀 직원분들.. 팀원분들과 기존 직원분들도 회사에 적응할 수 있도록 각자의 방법을 이용해 많은 도움을 주셨다.</p>
</li>
<li><p>적어도 우리팀은 신입에게 과한 요구를 하지 않는 것 같다. 팀이 생각하는 협업 스타일에 적응하는 것이 우선인 것 같다.</p>
</li>
<li><p>직원들이 출퇴근 시간에 민감하게 반응하지 않고 칼퇴가 가능하다. 출퇴근 시간에 지하철 복잡한게 싫어서 일부러 일찍 가는데, 자연스럽게 퇴근도 빨리한다. 아무도 뭐라 안하는데 오히려 너무 혼자 쌩 가버려서 눈치보인다. +) 9시 출퇴근은 지옥임.. 그 시간만큼은 꼭 피하자</p>
</li>
<li><p>재택하면 좋을 줄 알았는데 여름이라 그런지 덥기도 하고 많이 졸렸다. 할 일이 생기기 전까지는 출근하는게 더 좋을듯?</p>
</li>
<li><p>맥북은 무겁다. 충전기까지 들고다니면 더 무겁다.</p>
</li>
<li><p>명확하게 말할 줄 알아야겠다. 우리 팀 회의를 옆에서 구경했는데, 이렇게 군더더기 없이 깔끔한 회의는 처음봤다. 나는 장황하게 설명하는 경우가 많은데, 최대한 짧게 생각을 전달하면서도 모두가 알아들을 수 있도록 노력해야 팀 분위기에 엇나가지 않을 수 있을 것 같다.</p>
</li>
<li><p>미리디에서 나는 성장할 수 있을 것 같다.</p>
</li>
</ol>
<h2 id="한마디만-더">한마디만 더</h2>
<p>내가 작업한게 실제 서비스에 올라갈 생각을 하니 벌써 행복하다. 열심히 하겠습니다 :)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React의 forwardRef을 이용해 하위 컴포넌트의 element 참조하기 (Feat. callback ref)]]></title>
            <link>https://velog.io/@dev_2dong/React%EC%9D%98-forwardRef%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-%ED%95%98%EC%9C%84-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EC%9D%98-element-%EC%B0%B8%EC%A1%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dev_2dong/React%EC%9D%98-forwardRef%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-%ED%95%98%EC%9C%84-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EC%9D%98-element-%EC%B0%B8%EC%A1%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 15 Jun 2022 06:41:02 GMT</pubDate>
            <description><![CDATA[<h2 id="ref">ref</h2>
<p>함수 컴포넌트를 구성하는 여러 element중 일부를 참조하기 위해 <code>useRef()</code> hook과 <code>ref props</code>를 이용합니다. 주로 focus 효과를 주거나, input에서 텍스트를 긁어오는 등의 목적으로 사용했던 경험이 있습니다.</p>
<pre><code class="language-tsx">const Component = () =&gt; {
  const inputRef = useRef&lt;HTMLInputElement&gt;(null);

  useEffect(() =&gt; {
    inputRef.current?.focus();
  }, [])

  return (
    &lt;div&gt;
      &lt;input ref={inputRef}/&gt;
    &lt;/div&gt;
  )
}</code></pre>
<p>위의 코드에서 inputRef는 input element를 참조합니다. 개발자는 <code>current</code> 프로퍼티를 이용해 input에 접근 가능합니다.</p>
<h2 id="forwardref">forwardRef</h2>
<h3 id="문제-제기">문제 제기</h3>
<pre><code class="language-tsx">const Parent = () =&gt; {
  return (
    &lt;div&gt;
      &lt;Child/&gt;
    &lt;/div&gt;
  )
}

const Child = () =&gt; {
  return (
    &lt;div&gt;
      &lt;input/&gt;
    &lt;/div&gt;
  )
}</code></pre>
<p>위의 코드에서 Parent가 Child의 input에 접근하려면 어떻게 해야할까요? Parent에서 어떤 이벤트가 발생했을 때 Child의 input에 focus를 줘야한다면?</p>
<pre><code class="language-tsx">const Parent = () =&gt; {
  const childInputRef = useRef&lt;HTMLInputElement&gt;(null);

  return (
    &lt;div&gt;
      &lt;Child inheritRef={childInputRef}/&gt;
    &lt;/div&gt;
  )
}

const Child = ({ inheritRef }: Props) =&gt; {
  return (
    &lt;div&gt;
      &lt;input ref={inheritRef}/&gt;
    &lt;/div&gt;
  )
}</code></pre>
<p>위와 같이 Child 컴포넌트에서 지정해준 Props 인터페이스 정의에 따라 Parent 컴포넌트에서 props를 통해 ref를 전달할 수 있습니다. 하지만, 리액트에서 공식적으로 ref라는 props를 지원하는데 굳이 다른 이름으로 전달할 수 밖에 없는 상황에 대해 의문을 품을 수 있을 것 같습니다. </p>
<h3 id="forwardref-1">forwardRef</h3>
<p>위와 같은 상황을 해결하기 위해 리액트에서는 <code>forwardRef</code>를 제공합니다.</p>
<p>공식문서에서는 Ref Forwarding 기술을 아래와 같이 설명합니다.</p>
<blockquote>
</blockquote>
<p>(의역)
Ref forwarding은 자식 컴포넌트에게 ref를 넘겨주는 기술입니다. 
대부분의 컴포넌트에서는 사용하지 않아도 되지만, 재사용 가능한 컴포넌트와 관해서는 유용하게 사용할 수 있습니다.</p>
<p>사용 방법은 아래와 같습니다.</p>
<pre><code class="language-tsx">const Parent = () =&gt; {
  const inputRef = useRef&lt;HTMLInputElement&gt;(null);

  return (
    &lt;div&gt;
      &lt;Child ref={inputRef}/&gt;
    &lt;/div&gt;
  )
}


const Child = React.forwardRef&lt;HTMLInputElement&gt;((_, inputRef) =&gt; {
  return (
    &lt;div&gt;
      &lt;input ref={inputRef}/&gt;
    &lt;/div&gt;
  )
})</code></pre>
<p>React의 정적 메서드인 forwardRef의 인자로 기존 컴포넌트를 넣어주면 됩니다. </p>
<p>말은 기존 컴포넌트라고 했지만, 약간 다른 부분이 있습니다. Child 컴포넌트에서의 매개변수 선언부를 보면 <code>const Child = (_, inputRef) =&gt; {}</code> 와 같은 형태로 두번째 매개변수를 명시했는데요, 해당 매개변수가 바로 부모 컴포넌트에서 ref props로 넘겨준 ref 변수가 됩니다. 전달받은 ref 변수를 특정 element랑 연결할 수 있습니다.</p>
<h3 id="props와-함께-사용하기">props와 함께 사용하기</h3>
<p>만약, 해당 컴포넌트에서 props를 받기 위한 타입정의를 했다면 아래와 같이 코드를 작성하면 됩니다.</p>
<pre><code class="language-tsx">React.forwardRef&lt;HTMLInputElement, Props&gt;((props, ref) =&gt; {
// ...
})</code></pre>
<h2 id="3개-이상의-컴포넌트간-통신">3개 이상의 컴포넌트간 통신</h2>
<h3 id="문제-제기-1">문제 제기</h3>
<p>마지막으로, 제가 forwardRef를 정리하고자 했던 이유이자 포스팅을 하게 된 이유랑 연관있는 내용입니다. forwardRef를 통해 3개 이상의 컴포넌트에 연달아 ref를 전달하는 경우에 대해서입니다.</p>
<pre><code class="language-tsx">const GrandParent = () =&gt; {
  const inputRef = useRef&lt;HTMLInputElement&gt;(null);

  const handleInput = () =&gt; {
      // inputRef의 값을 참조해서 사용하는 코드 작성
  }

  return (
    &lt;div&gt;
      &lt;Parent ref={inputRef}/&gt;
      &lt;Button onClick={handleInput}/&gt;
    &lt;/div&gt;
  )
}

const Parent = React.forwardRef&lt;HTMLInputElement&gt;((_, inputRef) =&gt; {

  const handleInput = () =&gt; {
      // inputRef의 값을 참조해서 사용하는 코드 작성
  }

  return (
    &lt;div&gt;
      &lt;Child ref={inputRef}/&gt;
      &lt;Button onClick={handleInput}/&gt;
    &lt;/div&gt;
  )
})

const Child = React.forwardRef&lt;HTMLInputElement&gt;((_, inputRef) =&gt; {
  return (
    &lt;div&gt;
      &lt;input ref={inputRef}/&gt;
    &lt;/div&gt;
  )
})</code></pre>
<p>Child 컴포넌트의 input의 값을 Parent 및 GrandParent 컴포넌트에서 참조해야 하는 상황이 있었습니다. GrandParent 컴포넌트에서 input의 값을 참조하기 위해서는 평소 ref를 사용하는 것 처럼 쉽게 사용할 수 있었습니다.</p>
<pre><code class="language-tsx">// GrandParent
const handleInput = () =&gt; {
  const value = inputRef?.current?.value;
  // ...
}</code></pre>
<p>Parent 컴포넌트에서는 위와 같은 방식으로 참조할 경우 아래와 같은 오류가 나타납니다.</p>
<blockquote>
</blockquote>
<p>Property &#39;current&#39; does not exist on type &#39;((instance: HTMLInputElement | null) =&gt; void) | MutableRefObject&lt;HTMLInputElement | null&gt;&#39;.
Property &#39;current&#39; does not exist on type &#39;(instance: HTMLInputElement | null) =&gt; void&#39;.</p>
<p>forwardRef의 인자로 들어갔던 컴포넌트의 두번째 매개변수인 inputRef는 <code>(instance: HTMLInputElement | null) =&gt; void</code> 라는 함수일 수 있고, <code>MutableRefObject&lt;HTMLInputElement | null&gt;</code>의 Ref Object일 수 있다는 것입니다. 만약 받은 인자가 함수의 형태라면 current 프로퍼티가 없으므로 예외를 던지는 상황입니다.</p>
<p>해결은 아래와 같이 할 수 있습니다.</p>
<pre><code class="language-tsx">// Parent
const handleInput = () =&gt; {
  if(typeof inputRef !== &#39;function&#39;) {
    const value = inputRef?.current?.value;
    // ...
  }
}</code></pre>
<h3 id="forwardref의-타입">forwardRef의 타입</h3>
<p>문제 해결은 되었지만, useRef() hook에서 반환하는 타입과 forwardRef에서 전달받는 ref의 타입이 사뭇 달라보입니다. 이것을 알아보고 싶었습니다.</p>
<p>forwardRef에서 전달받는 타입은 <code>(instance: T) =&gt; void</code>와 <code>MutableRefObject&lt;T&gt;</code>이 Union으로 묶여있는 형태라는 것을 위의 케이스에서 볼 수 있었습니다. 즉, 둘 중 어떤것을 받아도 상관없다라는 의미로 해석할 수 있습니다.</p>
<p>useRef()에서 반환 가능한 값은 <code>MutableRefObject&lt;T&gt;</code>, <code>RefObject&lt;T&gt;</code>의 형태를 띄고 있습니다. 에디터에서 useRef를 클릭하고 F12를 누르면 타입 정의를 확인할 수 있는데요, 반환 타입은 아래와 같습니다.</p>
<pre><code class="language-tsx">function useRef&lt;T&gt;(initialValue: T): MutableRefObject&lt;T&gt;;
function useRef&lt;T&gt;(initialValue: T|null): RefObject&lt;T&gt;;
function useRef&lt;T = undefined&gt;(): MutableRefObject&lt;T | undefined&gt;;</code></pre>
<p>즉, ref props로 넘겨준 값은 MutableRefObject 꼴이기 때문에, 위의 경우처럼 타입을 체크해 ref를 사용할 수 있었습니다.</p>
<p><code>(instance: T) =&gt; void</code>와 같은 함수형 타입은 무엇일까요? 이것은 Callback Ref와 관련이 있습니다.</p>
<p>(* 참고 * RefObject 타입의 값도 ref props로 넘어가는 것을 확인했습니다. RefObject와 MutableRefObject의 차이는 내부의 current property가 RefObject의 경우 read-only라는 것 뿐입니다. 그런데 넘겨준 RefObject는 원래 current의 값을 변경하지 못하는게 맞다고 생각했는데 자식 컴포넌트에서 잘 바뀌더라고요..? 이 부분은 TS 언어적인 부분에 대한 미숙함때문에 생긴 궁금증인것 같아서 조금 더 공부해보려고 합니다.)</p>
<h3 id="callback-ref">callback ref</h3>
<p>일반적으로는 ref.current의 값이 변경되더라도 ref를 포함하고 있는 함수 컴포넌트를 다시 실행하지 않습니다. 리렌더링도 당연히 안됩니다. state나 props가 바뀌어 리렌더링이 되는 경우에만 ref에 저장되어있는 current 값을 이용해 화면을 다시 그릴 수 있습니다.</p>
<p>마찬가지로, ref가 특정 DOM element에 바인딩 되어도 컴포넌트가 리렌더링 되기 전까지는 상황을 알 수 없습니다.</p>
<pre><code class="language-tsx">const Component = () =&gt; {
    const ref = useRef&lt;HTMLDivElement&gt;(null);

      console.log(ref.current) // undefined;

      return &lt;div ref={ref} /&gt;
}</code></pre>
<p>예를 들어, 위와 같은 상황에서 ref에 div element를 바인딩했지만, ref.current의 변화는 컴포넌트 함수를 재호출하지 않기 때문에 컴포넌트 마운트 이후 별도의 메시지가 출력되지 않습니다.</p>
<p>마운트 이후에 ref로 받은 element를 이용한 작업을 수행해야 한다면 useEffect() hook을 이용해 동작을 정의할 수 있습니다.</p>
<pre><code class="language-tsx">const Component = () =&gt; {
    const ref = useRef&lt;HTMLDivElement&gt;(null);

      useEffect(() =&gt;  {
        // ref를 참조한 연산
    }, []);

      return &lt;div ref={ref} /&gt;
}</code></pre>
<p>리액트에서 제공하는 callback ref를 이용하면 보다 명료하고 깔끔하게 코드를 작성할 수 있습니다. ref props에 React.createRef()나 useRef()가 반환하는 값을 전달하는 것이 아니라, 접근하고 싶은 element를 매개변수로 가지는 콜백 함수를 ref props로 전달합니다.</p>
<pre><code class="language-tsx">const Component = () =&gt; {
      const callbackRef = useCallback((node: HTMLDivElement) =&gt;  {
        // node를 참조한 연산
    }, []);

      return &lt;div ref={callbackRef} /&gt;
}</code></pre>
<p>콜백함수의 매개변수로 들어온 node를 이용해, 컴포넌트가 렌더링 된 직후 동작을 정의할 수 있습니다. callback ref를 처음 들어봤기 때문에 또 어떤 활용을 할 수 있는지에 대해서는 더 알아봐야겠지만, 앞서 forward ref로 받을 수 있는 함수 형태의 타입이 callback 함수였다는 것은 알 수 있습니다.</p>
<h2 id="마무리">마무리</h2>
<p>저 역시 forwardRef을 알기 전까지는 자식 컴포넌트에서 정의했던 props명칭에 따라 ref를 전달하는 방식을 사용했었습니다. 이번에 해당 스펙을 처음 알게되었고, 앞으로는 통일성 있고 흐름에 맞는 코드를 작성하기 위해 많이 사용할 것 같습니다. 또한, forwardRef가 받는 인자의 타입에 의문을 가지면서 자연스레 callback ref에 대한 개념도 간단하게나마 익힐 수 있어 좋았습니다. 오히려 callback ref를 활용하는 코드를 앞으로 많이 작성하지 않을까 싶습니다.</p>
<p>이렇게 forwardRef에 대한 글을 작성했지만, 부모 컴포넌트에서 자식 컴포넌트 내부의 DOM을 직접 조작하는 것은 일반적인 컴포넌트 관계에서는 지양해야하는 패턴이라고 합니다. 여러 컴포넌트로 나누어 제작하는 것은 세부 기능을 숨기는 추상화를 하기위한 목적이 있기 때문인데, 다른 컴포넌트에서 자식 컴포넌트의 특성을 너무 쉽게 변경할 수 있다면 추상화가 잘 안된 컴포넌트이기 때문입니다. 오히려 이럴 경우에는 설계를 다시 고려해야 할 것 같습니다. (그런 측면에서, 3개의 컴포넌트를 forwardRef로 통신하려 했던 위와 같은 상황 자체에 문제가 있는 것 같네요.)</p>
<p>하지만, input이나 button처럼 외부에서 직접 DOM을 컨트롤하는 경우가 종종 있는데, 이럴 경우에 forwardRef을 사용해 관리하면 좋을 것 같습니다. 저도 이점에 염두해서 깔끔한 코드를 짤 수 있도록 항상 신경써야겠습니다.</p>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://reactjs.org/docs/forwarding-refs.html">https://reactjs.org/docs/forwarding-refs.html</a></li>
<li><a href="https://ko.reactjs.org/docs/refs-and-the-dom.html#callback-refs">https://ko.reactjs.org/docs/refs-and-the-dom.html#callback-refs</a></li>
<li><a href="https://leehwarang.github.io/2020/11/29/ref.html">https://leehwarang.github.io/2020/11/29/ref.html</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[<Git>버전관리를 해야하는 이유 및 여러가지 시스템]]></title>
            <link>https://velog.io/@dev_2dong/%EB%B2%84%EC%A0%84%EA%B4%80%EB%A6%AC%EB%A5%BC-%ED%95%B4%EC%95%BC%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0-%EB%B0%8F-%EC%97%AC%EB%9F%AC%EA%B0%80%EC%A7%80-%EC%8B%9C%EC%8A%A4%ED%85%9C</link>
            <guid>https://velog.io/@dev_2dong/%EB%B2%84%EC%A0%84%EA%B4%80%EB%A6%AC%EB%A5%BC-%ED%95%B4%EC%95%BC%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0-%EB%B0%8F-%EC%97%AC%EB%9F%AC%EA%B0%80%EC%A7%80-%EC%8B%9C%EC%8A%A4%ED%85%9C</guid>
            <pubDate>Sun, 17 Oct 2021 10:00:19 GMT</pubDate>
            <description><![CDATA[<h3 id="참고">참고</h3>
<ul>
<li>글이 두서가 없습니다^^.. (개선해볼게요!)</li>
<li>학습했던 내용, 파생된 궁금증, 개인적인 생각 등이 본문 사이사이에 끼어있습니다.</li>
<li>그래서 잘못된 정보가 많을 수 있습니다..!😥 </li>
<li>글 내용이나 제 생각이 잘못됐다면 댓글 남겨주세요. 같이 이야기해보고 싶습니다.</li>
<li>저작권 확인을 하고 사용하지만, 혹시나 사용한 자료의 저작권 문제가 있다면 댓글 남겨주세요. 바로 조치하겠습니다! </li>
</ul>
<h1 id="시작하며">시작하며</h1>
<p>오랜만에 팀을 이루어 개발을 하게 되었다. 평소처럼 브랜치 전략을 구상하고, 약속했던 흐름대로 소스코드를 합치기도 PR을 보내기도 했다. 하지만 얼마 지나지 않아 rebase and merge하는 부분에서 문제가 발생했다. 하지만 진짜 문제는 여태 git을 제대로 학습하지 않았던 나 자신으로부터 시작됐다. 팀원분께서 해당 이슈에 대한 의견을 제시했지만 동의하거나 반대하는 의견이 입 밖으로 나오지 않았던 것이다. 당시 팀원분의 해결 방안을 전혀 이해하지 못했으며, 방법대로 진행했을 때 어떤 결과가 나타날지 전혀 예상이 안 됐기 때문이다. 팀원분께 깃 공부 열심히 하겠다고 파워당당하게 말씀을 드렸고 12월쯤에서야 공부하기로 마음먹었던 학습을 땡기기로 결정했다.</p>
<p>앞으로 개인적으로나 팀 활동을 하면서 프로젝트 관리가 필요한 상황은 점점 더 많아질 것이다. 회사에 취직하게 된다면 그 중요성은 더 커질 것이며, 심지어 이슈가 발생했을 때 빠르게 이유를 파악하고 유연하게 대처할 수 있어야 할 것이다. 이번 학습의 목표는 아래와 같다.</p>
<ol>
<li>CLI 환경에서의 명령어 사용을 통해 git을 다룰 수 있도록 한다.</li>
<li>git이 동작하는 원리를 이해하고 설명할 수 있도록 한다.</li>
</ol>
<p>참고자료는 매 포스팅 하단에 작성할 것이지만, 베이스는 <a href="https://git-scm.com/book/ko/v2">git-scm</a>에서 제공하는 progit 교재를 통해 학습한다.</p>
<h1 id="키워드">키워드</h1>
<ul>
<li>버전 관리</li>
<li>버전 관리 시스템 (VSC, Version Control System)</li>
<li>로컬 버전 관리 시스템 (Local VSC)</li>
<li>중앙집중식 버전 관리 시스템 (CVCS, Centralized VSC)</li>
<li>분산 버전 관리 시스템 (DVCS, Distributed VSC)</li>
</ul>
<h1 id="버전-관리-시스템">버전 관리 시스템</h1>
<h2 id="버전-관리란">버전 관리란?</h2>
<p>문서작업을 한다고 가정한다. 
[고객의 이름, 연락처, 지역, 최근 방문일]의 데이터를 보관하라는 요구사항이 있었고, 이를 위해 스프레드 시트 편집기를 열고 데이터를 작성하고 저장한다.
<img src="https://images.velog.io/images/dev_2dong/post/caa4af9b-d9c8-44cb-a155-d8bbab01e945/image.png" alt="">
며칠 뒤, 개인정보 보관법 변경으로 인해 고객의 연락처를 앞으로 보관하면 안된다는 명령이 떨어졌다. 그로 인해, 문서의 연락처 열 자체를 삭제하기로 결정했다. 아래처럼 문서를 편집하고 저장한다.
<img src="https://images.velog.io/images/dev_2dong/post/0212e735-fa55-4a9d-ba42-d9b5cc329f96/image.png" alt="">
며칠 뒤, 해당 법이 3년 뒤부터 시행된다는 이야기를 전해 들었다. 다시 고객들의 연락처가 필요해진 상황이라서 해당 문서를 복원하려고 한다.</p>
<p>그런데, 이전 문서에 대한 정보는 어디에 있을까?
별도로 문서의 내용이 변경될 때마다 사본을 만들어두지 않았다면 과거의 작업 내용은 복원이 불가능하다. 
<img src="https://images.velog.io/images/dev_2dong/post/f997a7dc-4bce-4ad0-b7a4-71bc69b793b5/image.png" alt="">
왼쪽 사진은 사본을 만들어두지 않아 이전의 기록들을 찾아볼 수 없는 상황이다.
반면, 오른쪽 사진처럼 사본을 만들어뒀다면 과거의 기록들을 찾아보면서 이전에 사용했던 문서를 마치 지금 작업하던 것처럼 이어서 작업할 수 있다. 또한, 각 사본의 이름을 설정하는 등의 규칙으로 언제 변경된 문서인지 표기해두면 더 관리가 용이할 것이다. </p>
<p>여기서의 사본을 버전이라고 한다. 버전들을 관리하는 것을 통해 원하는 버전으로 작업 환경을 변경할 수 있으며, 원하는 시점의 작업 데이터를 다시 가져올 수도 있다.</p>
<h2 id="왜-필요한가">왜 필요한가?</h2>
<p>위의 상황은 스프레드 시트를 활용한 문서의 버전을 예시로 들었지만, 실제로 거의 대부분의 컴퓨터 파일들은 시간순서에 따른 변화를 기억해야 할 필요가 있다. 개발자라면 소스 코드 및 개발 문서를 시간순에 따라 버전 관리를 해둘 수 있다. 디자이너라면 디자인 설계서 및 도안 등을 관리할 수 있다. 그리고 이러한 버전관리의 개념은 매우 중요하다.</p>
<p>버전 관리 시스템의 이점은 이전 버전의 상태를 기억하는 것으로부터 파생된다. 버전에 따라 수정 내용을 비교해 볼 수도 있고, 실수로 파일을 훼손시켰다고 하더라도 쉽게 복구할 수 있다.</p>
<p>만약, 혼자서 작업하는 것이 아니라 팀 단위로 여러 명이 작업한다면 이러한 시스템을 통해 더 생산성 있는 관리를 할 수 있다. 예를 들어, 어떤 범위의 작업을 누가 했는지 알 수 있을 것이다. 어떤 오류가 발생했다면 누가 잘못을 했었는지 추적을 하는 것도 가능하다.</p>
<p>이러한 여러 이유들로 버전 관리가 필요하게 되며, 이러한 이점에 보다 가깝게 접근할 수 있는 체계가 연구되어왔다. 이를 버전 관리 시스템(Version Constrol System, VCS)이라고 한다.</p>
<p><strong>버전 관리 시스템의 장점</strong>은 아래와 같다.</p>
<ul>
<li>원하는 버전 상태로 자유롭게 움직일 수 있다.</li>
<li>작업물의 변경 이력을 관리할 수 있다.</li>
<li>버전에 따라 수정 내용을 비교할 수 있다.</li>
<li>누가 문제를 일으켰는지 추적이 가능하다.</li>
<li>파일을 잃어버리거나 잘못 고쳤을 경우에도 쉽게 복구가 가능하다.</li>
<li>예상하지 못한 문제가 발생 시 빠르게 대처 가능하다.</li>
</ul>
<h2 id="버전-관리-시스템-1">버전 관리 시스템</h2>
<p>버전 관리 시스템의 핵심 모티브는 당연 버전을 작업 순서대로 기억하고 이를 활용하는 것이다. 이러한 목적을 효과적으로 표현하기 위해 다양한 시스템이 형성되었다. 그리고, 이러한 시스템을 적절하게 사용하도록 돕는 여러 소프트웨어 도구들이 만들어졌다. 버전 관리를 대표하는 시스템들의 아이디어를 정리해보았다.</p>
<h3 id="로컬-버전-관리-시스템">로컬 버전 관리 시스템</h3>
<p>문서 작업을 했던 가정으로 돌아가 본다. 버전관리를 위해서 나름의 규칙을 정해놓고 사본을 만들어뒀다. 하지만, 해당 사본을 보관하던 디렉토리 자체를 실수로 삭제했다면 또다시 복원할 수 없는 문제가 발생한다. 
<img src="https://images.velog.io/images/dev_2dong/post/f7facacd-f942-424a-    8a05-dbd0bc9af564/local.png" width="440px"/></p>
<p>로컬 버전 관리 시스템은 이를 작은 데이터베이스에 저장하기로 했다. 해당 데이터베이스는 말 그대로 로컬환경인 내 자신의 컴퓨터 내부에 존재한다. 다만 저장하는 방식이 독특하다. 변경을 원하는 파일 전체를 저장하는 것이 아닌, <strong>마지막 버전으로부터 변경된 사항</strong>들만 차곡차곡 저장한다. 해당 변경사항을 <code>patch</code>라고 부른다. 변경사항만 가지고 있기 때문에 하나의 patch만을 가지고서는 올바른 버전을 불러올 수 없다.</p>
<p>특정 버전으로 돌아가고 싶다면, 로컬 VCS는 처음부터 patch를 순서대로 불러와서 합친다. 그리고 사용자는 합쳐진 문서를 이용하여 작업을 진행할 수 있다.</p>
<p>물론 여기에도 앞선 상황과 동일한 문제가 존재한다. 데이터베이스가 로컬에 있기 때문에, 만약 로컬 내부 데이터베이스에 문제가 생긴다면 관리하고 있던 버전에도 문제가 생길 수 있다는 것이다. 또한, 로컬 환경이기 때문에 협업을 고려했을 경우 효율적으로 사용할 수 없는 시스템이다.</p>
<p>위의 문제들을 요약하면 아래와 같다.</p>
<ul>
<li>로컬 데이터베이스에 문제가 발생할 경우 버전 관리 내역의 유지보수가 힘들어진다.</li>
<li>협업을 위해서 해당 방식을 이용할 수 없다.</li>
</ul>
<h3 id="중앙집중식-버전-관리-시스템-cvcs">중앙집중식 버전 관리 시스템 (CVCS)</h3>
<p>기존 로컬 버전 관리 시스템에서 제기된 문제를 해결하기 위해서는 데이터의 보관/유지 안정성에 집중할 수 있는 별도의 데이터베이스가 필요하며, 해당 데이터베이스를 여러 사람끼리 공유할 수 있으면 보완될 것으로 보인다.
<img src="https://images.velog.io/images/dev_2dong/post/e4b56e58-81db-4777-8d89-5ef7f35e2aba/centralized.png" width="440px"/></p>
<p>위의 사진처럼 모두가 공유할 수 있는 서버에 데이터베이스를 설치하면 버전 공유가 가능하다. 또한, 서버는 오로지 데이터베이스 관리의 임무만 가지고 있기 때문에 보다 더 안전하게 버전관리가 가능하다. 이 방식이 중앙집중식 버전 관리 시스템이다.(Centralized VCS, CVCS)</p>
<p><strong>중앙집중식 버전 관리 시스템의 장점</strong>은 아래와 같다.</p>
<ul>
<li>여러 사람들 간의 문서, 문서 버전의 공유가 가능하다.</li>
<li>중앙 데이터베이스를 하나만 관리하면 되므로 관리가 편하고 생산성이 향상된다.</li>
</ul>
<p>하지만 중앙 서버 하나만을 관리하기 때문에 문제가 발생하기도 한다. 만약 서버에서의 문제가 발생하여 접속 불능상태가 된 경우, 프로젝트의 현시점을 기록/저장(commit)하는 작업조차 불가능하게 된다. 마찬가지로 협업을 진행하던 다른 동료들의 작업을 받아올 방법도 없기 때문에 작업 진행에 큰 문제가 발생한다. 또한, 서버내의 데이터베이스 관리가 잘 되어야겠지만 혹시 하나 서버의 데이터베이스 일부가 변형된다면 이전의 버전들이 무의미해질 수 있다. (심지어 버전기록이 삭제된다면..?!)</p>
<h3 id="분산-버전-관리-시스템">분산 버전 관리 시스템</h3>
<p>중앙집중식 버전 관리 시스템에서는 서버의 버전관리에 문제가 생겼을 경우, 복원이 사실상 불가능하다. 왜 그럴까? 그 이유는 서버가 모든 변경 과정의 보관을 독점하고 있기 때문이다.</p>
<p>로컬의 컴퓨터는 서버에서 특정 버전의 파일을 가져와서 작업을 진행하고 있을 것이다. 다른 버전 상태에서의 스냅샷은 서버를 믿고 서버에 보관했기 때문에 로컬에는 버전과 관련된 그 어떠한 정보도 가지지 않는다. 따라서, 서버의 버전 기록들이 의도치 않게 변형된다면 이를 돌이킬 방법이 없다. 물론, 모든 버전의 스냅샷을 로컬 어딘가에 보관했다면 복원이 가능하다. 하지만, 중앙집중식 버전 관리 시스템을 사용한다면 애초에 그랬을 가능성이 매우 적었을 것이며 그럴 필요도 없었을 것이다. 왜냐하면, 로컬에 버전을 백업해놓는 방식은 로컬 버전 관리 시스템을 사용하는 방식보다 더 번거로울 것이며, 중앙집중식 버전 관리 시스템을 사용하는 목적을 포기하는 것과 같다고 생각했기 때문이다.
<img src="https://images.velog.io/images/dev_2dong/post/13fa9e7f-3575-4add-9cf5-e73cc04366f0/distributed.png" width="440px"/></p>
<p>이를 해결하기 위해 <code>분산 버전 관리 시스템</code>(DVSC, Distributed VSC)이 도입되었다. 분산 버전 관리 시스템은 서버에서 로컬로 특정 시점의 상태를 가져오지 않는다. 대신, 버전 관리되고 있던 저장소를 그대로 가져온다. 위의 그림을 보면 이해가 가능하다.</p>
<p>기존의 중앙집중식 버전 관리 시스템은 서버에서 파일의 버전을 확인하고, 원하는 버전의 파일을 가져온 뒤 로컬에서 작업을 진행했다. 그런데, 분산 버전 관리 시스템에서는 서버가 가지고 있는 버전 변경 이력들을 로컬로 전부 가지고 오는 것을 확인할 수 있다. 또한, 가져온 버전들 중에서 내가 원하는 시점의 파일을 가지고 작업하는 그림으로 이해할 수 있다.</p>
<p>이렇게 되면, 서버의 버전 관리 책임이 약간은 줄어든다. 서버에서 문제가 발생해서 데이터 유실이 있더라도 여러 사용자들이 로컬로 가져온 데이터베이스를 이용하여 복원할 수 있다. 또한, 버전들이 로컬에 존재하기 때문에 자신의 로컬에서 버전관리를 할 수 있다는 장점이 있다. 이전에는 버전관리를 위해서 무조건 서버를 거쳐야 했다. 따라서, 서버가 먹통이 되더라도 로컬에서 추가적인 버전관리를 하면 되기 때문에 무리가 덜하다. 심지어 인터넷이 안 되는 기차나 비행기 안에서도 작업 후 새로운 버전을 등록할 수 있게 된다는 이점이 있다.</p>
<p><strong>분산 버전 관리 시스템의 이점</strong>은 아래와 같다.</p>
<ul>
<li>중앙 서버에 접속 문제가 발생해도 로컬에서 관리가 가능하다.</li>
<li>중앙 서버의 데이터가 변형/유실되어도 로컬의 히스토리를 이용해 복원이 가능하다.</li>
<li>특정 버전의 파일만 가져오는 것이 아니기 때문에, 로컬 환경에서 안전하게 여러 가지 실험 및 테스트를 하는 것도 편하다.</li>
<li>로컬에서 버전 관리가 이루어지기 때문에 속도가 빠르다.</li>
<li>인터넷이 동작하지 않는 환경에서도 버전 관리가 가능하다.</li>
</ul>
<p>다만, 사용하기에 어렵다는 단점이 있어서 공부를 많이 해야 한다. 모르고 사용하면 여러 환경들이 꼬이게 되고 동기화 문제도 쉽게 발생한다. 대부분 개발자들이 사용하는 Git 역시 이에 속한다.</p>
<h1 id="마치며">마치며</h1>
<p>해당 개념을 이전에도 여러 번 학습했었지만, 역시나 대부분의 내용을 잊어버렸음을 이번에 공부하면서 다시 알게 되었다. 그리고 왜 분산 버전 관리 시스템이 도입됐어야 했는지도 학습정리를 꼼꼼하게 하면서 더 깊이 있게 생각해 볼 수 있었던 것 같다.</p>
<p>해당 글에서는 파일 하나를 예로 들었지만, 실제로는 이를 확장한 개념으로 디렉토리 내의 파일들 전부를 관리할 수도 있다. 더 나아가서 프로젝트에 사용되는 모든 디렉토리를 관리하는 것도 가능하다 (참고로 디렉토리도 파일이다). 앞으로는 이러한 가정으로 git에 대한 이야기를 정리할 생각이다.</p>
<h1 id="참고자료">참고자료</h1>
<h3 id="학습">학습</h3>
<ul>
<li><a href="https://git-scm.com/book/ko/v2/%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0-%EB%B2%84%EC%A0%84-%EA%B4%80%EB%A6%AC%EB%9E%80%3F">git-scm</a></li>
<li><a href="https://serengetitech.com/tech/introduction-to-git-and-types-of-version-control-systems/">Introduction to Git and Types of Version Control Systems</a></li>
<li><a href="https://medium.com/@kamilmasyhur/what-do-you-know-about-version-control-system-vcs-6a1e1922c970">What do you know about Version Control System</a></li>
<li><a href="https://coding-lks.tistory.com/162">git snapshot</a></li>
<li><a href="https://heekangpark.github.io/git/vcs">버전 관리 시스템</a></li>
</ul>
<h3 id="이미지-소스">이미지 소스</h3>
<pre><code>문제 있을 시 연락주시면 삭제하겠습니다.</code></pre><ul>
<li><a href="https://git-scm.com/book/ko/v2/%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0-%EB%B2%84%EC%A0%84-%EA%B4%80%EB%A6%AC%EB%9E%80%3F">git 버전관리와 관련된 이미지들</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[안드로이드 이미지 Tint변경이 적용되지 않는 경우 해결방법]]></title>
            <link>https://velog.io/@dev_2dong/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-Tint%EB%B3%80%EA%B2%BD%EC%9D%B4-%EC%A0%81%EC%9A%A9%EB%90%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EA%B2%BD%EC%9A%B0-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@dev_2dong/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-Tint%EB%B3%80%EA%B2%BD%EC%9D%B4-%EC%A0%81%EC%9A%A9%EB%90%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EA%B2%BD%EC%9A%B0-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Sun, 01 Nov 2020 06:30:08 GMT</pubDate>
            <description><![CDATA[<p>Vector Asset을 이용하여 넣어두었던 icon들의 동적인 색상 변화를 원했고,
그에 따라 일반적으로 생각되어지는 코드를 작성했다.</p>
<pre><code class="language-kotlin">view.setColorFilter(R.color.colorPrimary, PorterDuff.Mode.SRC_ATOP)</code></pre>
<p>하지만 적용되지 않았다.
색의 변화는 있었지만, 원하는 색이 아닌 거무스름한 색이 나왔다.</p>
<p>아래와 같은 코드로 수정했더니 원하는 결과가 나왔다.</p>
<pre><code class="language-kotlin">val drawable: Drawable = DrawableCompat.wrap(view.drawable) // getDrawable()

DrawableCompat.setTint(
    drawable.mutate(),
    ContextCompat.getColor(this, R.color.colorPrimary)
)</code></pre>
<p>됐다고 끝낼 수는 있지만, 코드를 간단하게는 분석해 볼 필요가 있었다.</p>
<h3 id="wrap">wrap()</h3>
<p>우선 궁금했던건, <code>DrawableCompat.wrap(@params Drawable)</code> 의 역할이었다.
함수를 파고 들어가면, 아래와 같은 코드를 볼 수 있다.</p>
<pre><code class="language-java">public static Drawable wrap(@NonNull Drawable drawable) {
    if (Build.VERSION.SDK_INT &gt;= 23) {
        return drawable;
    } else if (Build.VERSION.SDK_INT &gt;= 21) {
        if (!(drawable instanceof TintAwareDrawable)) {
            return new WrappedDrawableApi21(drawable);
        }
        return drawable;
    } else {
        if (!(drawable instanceof TintAwareDrawable)) {
            return new WrappedDrawableApi14(drawable);
        }
        return drawable;
    }
}</code></pre>
<p>매개값으로 Drawable 객체가 들어오는데, 리턴값 역시 Drawable이다.
또한, SDK 버전별로 Drawable 객체가 다른 방식으로 래핑됨을 알 수 있는데, 반환되는 각각의 객체 역시 Drawable을 상속받은 클래스로부터 생성된 인스턴스로 이해할 수 있다.</p>
<p>이렇게 버전별로 Drawable의 처리를 받음으로, 버전에 상관없이 내부의 Tint 변경 코드를 사용할 수 있게 된다.</p>
<h3 id="settint">setTint()</h3>
<p>DrawableCompat의 setTint 함수를 사용한다. 첫 매개변수로 Drawable 객체를, 두 번째 매개변수로 color값을 int로 받는다.</p>
<p>위의 코드를 보면 알 수 있겠지만, Drawable 객체를 지정한 색으로 바꿔주는 역할을 하는 직관적인 함수다.</p>
<h3 id="mutate">mutate()</h3>
<p>Android에서는 자체의 성능 향상을 위해 공통된 Drawable 객체의 리소스 파일을 사용하는 여러 뷰들에 대해, 공통된 하나의 상태만을 공유하도록 되어있다. 즉, 하나의 tint값을 바꾸면, 이를 사용하는 다른 뷰에 대해서도 tint값이 바뀐 상태로 우리에게 보여진다는 의미다.</p>
<p>이에 대한 설정을 꺼주고, 각 뷰들이 다른 뷰에 영향을 받지 않도록 도와주는 함수가 mutate() 이다.</p>
<h3 id="결론">결론</h3>
<p>글을 쓰고 공부를 하다보니 본 글은 DrawableCompat을 활용한 Tint변경에 대한 글이 맞는 것 같다.</p>
<p>이는 버전별 대응을 통해 색 변화가 이루어지도록 구성된 API클래스 이므로, 
서두에서 언급했던 setColorFilter()가 작동하지 않는 문제의 해결법으로서의 글로는 적합하지 않다. (물론 문제가 해결되긴 했지만..)</p>
<p>setColorFilter()가 먹히지 않는 문제에 대해서는 공부를 더 해본 뒤 정리를 해봐야겠다.</p>
<h3 id="참고자료">참고자료</h3>
<p><a href="https://medium.com/@hanru.yeh/tips-for-drawablecompat-settint-under-api-21-1e62a32fc033">https://medium.com/@hanru.yeh/tips-for-drawablecompat-settint-under-api-21-1e62a32fc033</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Android SharedPreferences Observing]]></title>
            <link>https://velog.io/@dev_2dong/Android-SharedPreference-Observing</link>
            <guid>https://velog.io/@dev_2dong/Android-SharedPreference-Observing</guid>
            <pubDate>Sun, 11 Oct 2020 07:14:08 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요. 이동현입니다.</p>
<p>어디서나 접근 가능한 간단한 지속성 데이터를 저장하는 방식으로 <code>SharedPreferences</code>를 사용 하곤 합니다.</p>
<p>최근 프로젝트를 진행하면서,
저장된 SharedPreference값이 바뀌는 것을 Observe하고, 그 즉시 데이터의 변경에 따른 View 단에서의 처리를 해야 할 일이 있었습니다.</p>
<p>LiveData를 통한 처리가 될 것 같았지만, 생각보다 쉽지 않았습니다.
구글링을 통해 방법을 알아냈고, 정리 + 기억 보존겸 포스팅을 하려고 합니다.</p>
<p>본 방법은 RxJava / RxAndroid를 사용합니다.
저는 이 부분을 학습해본적이 없어서 코드에 대한 분석보다는, 간단한 예제 정도만 보여주고 끝날 것 같습니다.</p>
<h1 id="0-샘플-앱에-대한-설명">0. 샘플 앱에 대한 설명</h1>
<p>EditText에 입력된 문자열을 &#39;저장하기&#39; Button을 눌러 SharedPreference의 Value로 저장합니다.
이 때, SharedPreference의 Value가 변경됨을 감지하여, 변경된 문자열을 별도의 TextView에 출력해서 사용자에게 보여주는 샘프 앱을 만듭니다.</p>
<p>이 때, Button의 onClick() 메서드 내에, TextView를 조작하는 과정이 없다는 것을 확인하면서 진행합니다.</p>
<p>activity_main.xml 의 코드는 아래와 같습니다.</p>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;LinearLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;
    android:orientation=&quot;vertical&quot;
    android:padding=&quot;24dp&quot;
    tools:context=&quot;.MainActivity&quot;&gt;

    &lt;TextView
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:layout_marginTop=&quot;64dp&quot;
        android:text=&quot;SharedPreference 저장값&quot;
        android:textSize=&quot;24sp&quot; /&gt;

    &lt;TextView
        android:id=&quot;@+id/tv_name&quot;
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:layout_marginTop=&quot;8dp&quot;
        android:textSize=&quot;48sp&quot; /&gt;

    &lt;EditText
        android:id=&quot;@+id/input_name&quot;
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:layout_marginTop=&quot;64dp&quot;
        android:hint=&quot;SharedPreference에\n저장할 값을 입력하세요&quot;
        android:textSize=&quot;20sp&quot; /&gt;

    &lt;Button
        android:id=&quot;@+id/action_save&quot;
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:layout_marginTop=&quot;8dp&quot;
        android:text=&quot;저장하기&quot;
        android:textSize=&quot;16sp&quot;/&gt;

&lt;/LinearLayout&gt;</code></pre>
<h1 id="1-rxjavaandroid-dependency">1. RxJava/Android dependency</h1>
<pre><code>implementation &#39;io.reactivex.rxjava2:rxjava:2.1.9&#39;
implementation &#39;io.reactivex.rxjava2:rxandroid:2.0.2&#39;</code></pre><p>위의 dependency들을 gradle(app)에 넣어줍니다.</p>
<h1 id="2-사용할-sharedpreference-준비">2. 사용할 SharedPreference 준비</h1>
<h3 id="1-sharedpreference">1) SharedPreference</h3>
<pre><code class="language-kotlin">class SharedPreference(context: Context) {
    private val sharedPreference = 
       context.getSharedPreferences(&quot;observe_test&quot;, MODE_PRIVATE)

    var name: String?
        get() = sharedPreference.getString(&quot;TEST_NAME&quot;, &quot;&quot;)
        set(value) = sharedPreference.edit().putString(&quot;TEST_NAME&quot;, value).apply()
}</code></pre>
<p>위와 같이 기본적인 SharedPreference를 만들어둡니다.</p>
<p>EditText에 입력된 값은 <code>TEST_NAME</code>을 Key로 가지도록 구성했습니다.
저장되는 파일의 이름은 <code>observe_test</code>입니다.</p>
<h3 id="2-application의-상속-클래스-작성">2) Application의 상속 클래스 작성</h3>
<p>또한, SharedPreference를 어디에서나 사용하기 위해 Application을 상속받은 클래스에서 아래와 같은 작업을 해줍니다.</p>
<pre><code class="language-kotlin">class App: Application() {
    companion object {
        lateinit var sharedPreference: SharedPreference
    }

    override fun onCreate() {
        super.onCreate()
        sharedPreference = SharedPreference(applicationContext)
    }
}</code></pre>
<p>이제, Application.sharedPrefernce 의 방식으로 SharedPreference에 접근 가능합니다.</p>
<h3 id="3-manifestsxml-등록">3) Manifests.xml 등록</h3>
<p>마지막으로, manifests 파일에 만들어진 App Class를 등록합니다.</p>
<pre><code class="language-xml">&lt;application
    android:name=&quot;App&quot;</code></pre>
<h1 id="3-관찰-작업을-위한-livedata-class의-확장">3. 관찰 작업을 위한 LiveData Class의 확장</h1>
<p>LiveData의 상속을 통해 Observer 사용을 Custom 할 수 있습니다.</p>
<p>아래와 같은 코드를 사용합니다.</p>
<pre><code class="language-kotlin">class LivePreference&lt;T&gt; constructor(
    private val updates: Observable&lt;String&gt;,
    private val preferences: SharedPreferences,
    private val key: String,
    private val defaultValue: T
) : MutableLiveData&lt;T&gt;() {

    private var disposable: Disposable? = null

    override fun onActive() {
        super.onActive()

        value = (preferences.all[key] as T) ?: defaultValue

        disposable = updates
            .filter { t -&gt; t == key }
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribeWith(object: DisposableObserver&lt;String&gt;() {
                override fun onComplete() {
                }

                override fun onNext(t: String) {
                    postValue((preferences.all[t] as T) ?: defaultValue)
                }

                override fun onError(e: Throwable) {
                }
            })
    }

    override fun onInactive() {
        super.onInactive()
        disposable?.dispose()
    }

}</code></pre>
<p>만들어진 클래스를 보시면 <code>MutableLiveData</code>를 상속받고 있습니다.</p>
<p>관찰 상태에 따른 처리를 override 된 함수를 통해 할 수 있습니다.
관찰자가 존재한다면 <code>onActive()</code>가 호출되도록, 관찰 상태가 해체됨에 따라 <code>onInactive()</code>가 호출되도록 합니다.</p>
<p>특히, onActive()의 <code>setValue()</code>를 통해, 값을 업데이트하고 모든 관찰자에게 이 사실을 알릴 수 있습니다. 이는 기존의 LiveData에서의 observing과 같은 방식입니다.</p>
<h1 id="4-observable-sharedpreference-만들기">4. Observable SharedPreference 만들기</h1>
<p>실제로 Activity 에서 사용할  LiveSharedPreference를 만들어줍니다.
생성자로, Base가 될 SharedPreference를 받아줍니다.</p>
<pre><code class="language-kotlin">class LiveSharedPreferences constructor(private val preferences: SharedPreferences) {
    private val publisher = PublishSubject.create&lt;String&gt;()
    private val listener = SharedPreferences
        .OnSharedPreferenceChangeListener { _, key -&gt; publisher.onNext(key) }

    private val updates = publisher.doOnSubscribe {
        preferences.registerOnSharedPreferenceChangeListener(listener)
    }.doOnDispose {
        if (!publisher.hasObservers()) {
            preferences.unregisterOnSharedPreferenceChangeListener(listener)
        }
    }

    fun getString(key: String, defaultValue: String): LivePreference&lt;String&gt; {
        return LivePreference(updates, preferences, key, defaultValue)
    }
}</code></pre>
<p>PublishSubject  이 부분이 RxJava를 사용한 방식입니다.
이해를 못한 부분이기도 합니다.</p>
<p>코드만 남기고, 후에 RxJava 학습이 된다면 부가적인 설명을 하겠습니다.</p>
<hr>
<p><code>getString()</code> 함수에 parameter로 key와 defaultValue를 받습니다.
사용된 함수는 앞서 만들었던 LivePreference 클래스를 return하며, observer를 달아줄 시 LiveData에서 동작하던 방식 그대로 값을 관찰합니다.</p>
<h1 id="5-사용하기">5. 사용하기</h1>
<p>MainActivity에서의 사용입니다. 아래와 같이 사용합니다.</p>
<pre><code class="language-kotlin">class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val sharedPreference = 
            getSharedPreferences(&quot;observe_test&quot;, Context.MODE_PRIVATE)
        val liveSharedPreference = LiveSharedPreferences(sharedPreference)


        // Observer 달아주는 과정
        liveSharedPreference
            .getString(&quot;TEST_NAME&quot;, &quot;&quot;)
            .observe(this, Observer&lt;String&gt; { result -&gt;
                tv_name.text = result
            })


        // Button 클릭 리스너
        action_save.setOnClickListener {
            App.sharedPreference.name = input_name.text.toString()
        }
    }
}</code></pre>
<p>LiveSharedPreferences의 생성자로 SharedPreference를 넣어야 합니다.
따라서, 각각의 프로퍼티를 만들어 초기화 시켜줍니다.</p>
<p>사용하는 방식은 주석으로 달아두었습니다.
만들어준 getString() 메서드를 사용하여 LiveData로 감싸진 String을 가져오며, 이를 observe하여 그에 따른 후속 작업을 처리하도록 설계되었습니다.</p>
<p>여기서 주의깊게 보아야 하는 부분은, Button의 클릭 리스너 등록 부분입니다.
이 부분에서는 오로지 SharedPreference의 문자열 값을 새로 Update 해주는 기능밖에 하지 않습니다.
문자열 값이 업데이트 되면, observer가 이를 감지하여, 변경된 값을 result 변수로 받아줍니다. 이후, TextView에 result 문자열을 띄워주게 됩니다.</p>
<h1 id="실행-화면">실행 화면</h1>
<image src="https://images.velog.io/images/dev_2dong/post/c6b2548f-ee49-417a-a14f-d5859db8c947/1.jpg" style="width: 200px;"/>

<image src="https://images.velog.io/images/dev_2dong/post/1a38b640-be9c-4f95-b716-6198072eb3b6/2.jpg" style="width: 200px;"/>

<p>정상적으로 작동하는 것을 확인할 수 있습니다.</p>
<hr>
<p>사용법만 정리해두었고, 확실한 코드 이해가 언젠가 된다면 글을 수정 할 생각입니다.
(곧 SharedPreferences도 버려질 가능성이 크지만..)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin] #001. 변수]]></title>
            <link>https://velog.io/@dev_2dong/Kotlin-001.-%EB%B3%80%EC%88%98</link>
            <guid>https://velog.io/@dev_2dong/Kotlin-001.-%EB%B3%80%EC%88%98</guid>
            <pubDate>Sun, 27 Sep 2020 04:34:04 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Kotlin의 모든 문법과 기능을 완벽하게 이해하고 싶어 작성하는 글입니다.
잘못된 지식 및 오타가 있을 시 댓글로 알려주시면 감사하겠습니다😎</p>
</blockquote>
<p>안녕하세요 이동현입니다.</p>
<p>Kotlin에서의 변수에 대해 공부를 하면서 정리해보았습니다.</p>
<h1 id="변수">변수</h1>
<h3 id="예시">예시</h3>
<p>변수란 무엇일까요? 예를 통해 접근하면 이해가 쉽습니다.</p>
<p>간단한 계산기 프로그램을 만든다고 가정해봅니다.
&#39;더하기&#39; 기능을 구현 할 계획이라면, 아래와 같은 로직을 생각 해 볼 수 있습니다.</p>
<ol>
<li>한 개의 숫자를 입력받습니다.</li>
<li>또 하나의 숫자를 입력받습니다.</li>
<li>입력받은 두개의 수를 더해줍니다.</li>
<li>결과를 화면에 출력해줍니다.</li>
</ol>
<p>프로그램은 1, 2번 과정에서 입력받은 각각의 숫자들을 <code>어떠한 공간</code>에 담아두는 방식으로 기억을 하고 있어야 합니다. (더하는 과정에서 기억했던 값을 꺼내어 사용해야 하기 때문입니다.)
또한, 3번 과정에서 더한 결과값을 <code>어떠한 공간</code>에 담아두어야 4번 과정에서 출력시 사용할 수 있습니다.</p>
<h3 id="🤔어떠한-공간">🤔어떠한 공간?</h3>
<p>입력받은 값, 출력할 값 등을 저장하는 공간이 기기 내부적으로 필요합니다.
수치 혹은 자료등을 저장하는 기기의 <code>메모리</code>가 그 부분을 담당하기에 적합합니다.</p>
<p>위에서 언급했던 <code>어떠한 공간</code>은 <code>메모리</code>를 의미합니다.</p>
<p>즉, 프로그램에서는 사용할 여러 값들을 <code>메모리</code>에 저장하게 됩니다.</p>
<h3 id="변수란">변수란?</h3>
<p>값을 저장하려면 메모리가 필요하다는 것을 알았습니다.
하지만, 메모리는 매우 넓은 공간이며, 우리가 넣어줄 데이터는 상대적으로 아주 작은 양입니다.</p>
<p>메모리에 값을 넣어주면, 그 값이 들어간 <code>공간</code>이 생기게 됩니다.
그 <code>공간</code>의 주소/위치를 알고있어야, 나중에 공간에 접근하여 값을 사용할 수 있습니다.</p>
<p><code>변수</code>는 그러한 <code>공간의 주소</code>를 기억하는 역할을 합니다.</p>
<h1 id="변수의-선언">변수의 선언</h1>
<h3 id="변수의-선언이란">변수의 선언이란?</h3>
<p>변수를 프로그램에서 사용 및 활용하기 위해서는 아래와 같은 과정을 거쳐야합니다.</p>
<ol>
<li>어떤 <code>값</code>을 저장을 할까?</li>
<li>저장한 값이 하는 역할을 쉽게 기억하기 위한 <code>이름</code>을 정해야 한다.</li>
</ol>
<p>값을 메모리에 저장하면 공간은 자동으로 할당됩니다.
따라서, 우리는 &#39;어떤 <code>값</code>을 저장 할 것이냐&#39;만 생각해주면 됩니다.</p>
<p>후에 변수에 저장된 값을 사용하려면 어떻게 해야할까요?
해당 값이 들어있는 메모리 공간 주소에 접근을 해야하고, 그 주소에 들어있는 값을 꺼내와야합니다.
이 때, 변수의 <code>이름</code>을 지정해주면, 메모리 공간 주소에 이름을 통해 접근이 가능해집니다.</p>
<p>이러한 일련의 과정을 <code>변수를 선언</code>한다고 부릅니다.</p>
<h3 id="예시-1">예시</h3>
<p>위의 계산기 프로그램을 다시 보겠습니다.
그 중, 첫번째 숫자의 정보를 변수로 선언하는 아이디어를 보겠습니다.</p>
<h4 id="1-어떤-값을-저장을-할까">1. 어떤 값을 저장을 할까?</h4>
<p>첫 번째로 입력받을 숫자를 10으로 두고 싶습니다.
→ 10</p>
<h4 id="2-저장한-값이-하는-역할을-쉽게-기억하기-위한-이름을-정하자">2. 저장한 값이 하는 역할을 쉽게 기억하기 위한 이름을 정하자</h4>
<p>첫 번째 숫자임을 명시적으로 나타내는 이름이 좋을 것 같습니다.
→ firstNumber</p>
<h3 id="연결을-어떻게-할까">연결을 어떻게 할까?</h3>
<p>변수의 연결은 아래와 같은 형식으로 이루어집니다.</p>
<pre><code class="language-kotlin">1) val 이름 = 값
2) var 이름 = 값</code></pre>
<p>앞에 존재하는 <code>val</code>과 <code>var</code>은 변수를 선언한다는 의미를 가진 키워드입니다.
두가지 방법이 있음을 알 수 있는데, 둘의 차이는 다음 포스팅에서 이야기를 해 볼 생각입니다.</p>
<p>따라서, 앞서 계산기에 입력 할 첫 번째 숫자 정보를 저장하는 변수의 선언은 아래와 같이 행해질 수 있습니다.</p>
<pre><code class="language-kotlin">val firstNumber = 10
var firstNumber = 10</code></pre>
<p>마찬가지로, 두 번째 숫자 정보의 저장도 가능합니다.</p>
<pre><code class="language-kotlin">val secondNumber = 20
var secondNumber = 20</code></pre>
<h1 id="변수의-사용">변수의 사용</h1>
<p>앞서, 값을 저장하는 방식에 대해 살펴보았습니다.
그렇다면, 저장한 값을 사용하는 방법에 대해 알아 볼 필요가 있습니다.</p>
<p>저장한 값을 사용하려면, 값을 저장했던 변수의 이름을 알고 있어야합니다.
변수의 이름을 직접 사용하면, 기억하고 있던 공간의 주소에 접근하여 값을 불러옵니다.</p>
<h3 id="예시-2">예시</h3>
<p>계산기 프로그램을 이어서 만들어봅니다.
3번 과정에서는 입력받은 두개의 수를 더해주는 과정을 거칩니다.</p>
<p>입력받은 두개의 수는 각각 <code>firstNumber</code>, <code>secondNumber</code>  라는 이름을 가진 변수를 사용해서 값을 저장 해두었습니다.</p>
<p>값을 사용하기 전에, 결과값을 담는 변수를 하나 만들어보도록 하겠습니다.
덧셈 결과를 저장하는 것이 목적이기 때문에 <code>resultSum</code>이라는 이름을 선택했습니다.</p>
<pre><code class="language-kotlin">val resultSum = 덧셈결과
var resultSum = 덧셈결과
</code></pre>
<p>덧셈결과에는 값이 들어가게 되는데, 여기에서의 값은 firstNumber에 저장된 값을 호출하고,  secondNumber에 저장된 값을 호출하여 서로 더해준 결과가 됩니다.</p>
<p>변수의 이름을 직접 사용해서 값을 불러주면 됩니다.</p>
<pre><code class="language-kotlin">val resultSum = firstNumber + secondNumber
var resultSum = firstNumber + secondNumber</code></pre>
<h3 id="마무리">마무리</h3>
<p>마지막으로, 결과를 출력하면 프로그램 수행이 완료됩니다.</p>
<pre><code class="language-kotlin">print(resultSum)</code></pre>
<p>이 또한, 변수 이름을 단순히 사용만 해서 값을 불러냈습니다.</p>
<h1 id="가능한-값들">가능한 값들</h1>
<p>위의 예에서는 숫자 값만을 변수의 값으로 넣어주었습니다.
하지만 숫자이외에 문자, 배열.. 혹은 다양한 계산 식 역시 값으로 할당 가능합니다.</p>
<p>간단한 상황을 우선적으로 제시합니다.</p>
<pre><code class="language-kotlin">// 정수
val number1 = 10
val number2 = -10

// 실수
val number3 = 3.14
val number4 = -3.14

// 문자
val word1 = &#39;A&#39;
(X) val wrongWord = &#39;ABCD&#39; 

// 문자열
val word2 = &quot;A&quot;
val word3 = &quot;ABCD&quot;

// 논리
val boolean1 = true
val boolean2 = false</code></pre>
<p>특히나, 문자와 문자열은 다른 개념이므로 표현 방식을 확실히 구분해야 합니다.
<code>0개 이상의 여러 문자</code>가 합쳐져 <code>문자열</code>이 만들어집니다.</p>
<p>문자는 작은 따옴표 사이에, 문자열은 큰 따옴표 사이에 값을 입력하면 됩니다.
따라서, 두 개 이상의 문자를 작은 따옴표 사이에 담을 수 없습니다.</p>
<p>복잡한 상황에 대해서는 나올 때 마다 정리를 하도록 하겠습니다.</p>
<h1 id="변수-이름-명명-규칙">변수 이름 명명 규칙</h1>
<p>프로그래밍은 값/데이터의 연속적이고 지속적인 관리를 통해 이루어집니다.
그 값을 저장하고 사용하는 수단으로 변수가 사용됩니다.</p>
<p>결국, 변수를 선언하는 지점은 관리할 데이터의 시작이 되는것이며, 계속해서 데이터를 관리하기 위해서는 접근을 어렵게 만들어 주어서는 안됩니다.</p>
<p>따라서, 변수이름을 잘 정하는 것이 매우 중요합니다.</p>
<p>일반적인 규칙들이 존재합니다.</p>
<ul>
<li>첫 번째 글자는 문자이다. 숫자로 시작해서는 안된다.
(단, &#39;_&#39; 기호는 첫 글자로 사용 가능하다.)</li>
<li>영어 대소문자를 구분한다. (ex firstNumber, FirstNumber는 다른 변수)</li>
<li>길이의 제한은 없다.</li>
<li>첫 문자는 소문자로 시작하며, 여러 단어가 연결되어 있을 경우 두 번째 단어부터의 첫 문자는 
대문자로 한다. (Camel 표기법)</li>
<li>예약어는 변수명으로 사용할 수 없다.</li>
</ul>
<h1 id="다음-포스팅">다음 포스팅</h1>
<p>변수 값을 저장하는 디테일한 방식에 대한 글을 작성할 계획입니다.</p>
<ul>
<li>자료형</li>
<li>val, var</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[안드로이드 Listener Interface 구현 과정에 대한 이해]]></title>
            <link>https://velog.io/@dev_2dong/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-Callback-%EA%B5%AC%ED%98%84%EC%97%90-%EB%8C%80%ED%95%9C-%EC%9D%B4%ED%95%B4</link>
            <guid>https://velog.io/@dev_2dong/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-Callback-%EA%B5%AC%ED%98%84%EC%97%90-%EB%8C%80%ED%95%9C-%EC%9D%B4%ED%95%B4</guid>
            <pubDate>Sun, 13 Sep 2020 13:41:18 GMT</pubDate>
            <description><![CDATA[<h1 id="도입">도입</h1>
<p>안녕하세요. 이동현입니다.
이번에 다루어 볼 내용은 Callback과 Listener입니다.</p>
<p>안드로이드에서는 앱 내의 뷰(Button, EditText등)들과 사용자가 상호작용을 하기 위해 Callback 함수를 정말 많이 사용하고 구현합니다.
입문에서부터 &#39;버튼 클릭시 이벤트 처리&#39;와 같은 매우 간단한 Callback Pattern을 구현하게 되는 것을 그 근거로 들 수 있습니다.</p>
<p>이번에 직접 Callback을 구현해 볼 일이 있었습니다. 매우 중요한 개념이므로 정리가 필요하다 느껴져 포스팅을 시작하기로 했습니다.</p>
<p>실제 Android Framework에서는 이러한 구조를 어떻게 코드로 나타냈는지 Button의 Click Event처리 과정을 통해 살펴보겠습니다.
또한, RecyclerView와 Activity간의 상호작용을 목적으로 한 Callback Pattern을 직접 구현해보겠습니다.</p>
<h1 id="button의-click-event-처리">Button의 Click Event 처리</h1>
<p>흔히 사용하는 Button의 Click 이벤트를 처리하기 위해서는, 이를 처리하기 위한 함수와 객체가 필요합니다.</p>
<ol>
<li>사용자와 상호작용을 하게 될 Button 객체</li>
<li>Button의 Click Event를 감지하는 Listener 객체</li>
<li>Event 감지 이후, 로직의 처리등을 위해 실행되는 Callback 함수</li>
</ol>
<h3 id="1-사용자와-상호작용을-하게-될-button-객체">1. 사용자와 상호작용을 하게 될 Button 객체</h3>
<p>Button을 안드로이드 앱 UI로서 시각적으로 보이게 하기 위해서는 layout resource에 다음과 같은 xml을 작성 해야 합니다.</p>
<pre><code class="language-xml">&lt;Button
    android:id=&quot;@+id/test_button&quot;
    android:layout_width=&quot;wrap_content&quot;
    android:layout_height=&quot;wrap_content&quot;
    android:text=&quot;버튼&quot;/&gt;</code></pre>
<blockquote>
<p>Kotlin Extension에서는 자체 제공하는 Synthetic Binding을 통해, 특별한 변수의 선언 없이 Button 객체를 사용할 수 있도록 도와줍니다.</p>
</blockquote>
<p>이후 Activity를 관리하는 코드에서 Button 객체를 생성해 줄 수 있습니다.</p>
<pre><code class="language-kotlin">val testButton = findViewById&lt;Button&gt;(R.id.test_button)</code></pre>
<p>이를 통해 Button 객체를 만들고 사용할 수 있습니다.</p>
<h3 id="2-button의-click-event를-감지하는-listener-객체">2. Button의 Click Event를 감지하는 Listener 객체</h3>
<p>화면상에 보이는 View들은 대부분 사용자와 상호작용을 하도록 되어 있습니다.
가장 많이 활용되는 <code>클릭 이벤트</code> 역시 마찬가지입니다.</p>
<p>그렇기 때문에, Button 클래스는 대부분의 View들이 사용하고 있는 기능이 모여있는 View Class의 멤버들을 사용하기 위해 View Class를 상속받습니다.</p>
<p>이러한 Click Event Listener를 구현하도록 도와주는 Interface 역시 View Class에 존재합니다.</p>
<pre><code class="language-java">/**
 * Interface definition for a callback to be invoked when a view is clicked.
 */
public interface OnClickListener {
    /**
     * Called when a view has been clicked.
     *
     * @param v The view that was clicked.
     */
    void onClick(View v);
}</code></pre>
<p style="color: #4444">@ View Class의 OnClickListener Interface</p>
<br>


<p>주석을 통해 몇 가지 정보를 알 수 있습니다.
위 코드는 View가 클릭 되었을 경우 호출되는 콜백에 대한 인터페이스를 정의한 것이며,
호출되는 함수는 내부의 &#39;onClick()&#39; 함수가 됩니다. </p>
<p>위의 주석에 따르면, onClick() 함수는 결국 View가 클릭 되었을 경우 불려진다고 합니다. View가 클릭 되었을 경우 onClick() 함수를 호출하는 코드 역시 View 클래스 내부에 존재합니다.</p>
<pre><code class="language-java">public boolean performClick() {
    ...
    final ListenerInfo li = mListenerInfo;
    if (li != null &amp;&amp; li.mOnClickListener != null) {
        li.mOnClickListener.onClick(this);
        ...
    } else {
        ...
    }
    ...
}</code></pre>
<p style="color: #4444">@ View Class의 performClick()</p>
<br>

<p>코드를 살펴보면, performClick() 함수에서는 listener정보를 관리하는 ListenerInfo 정적 객체의 정보를 가져옵니다. 등록된 OnClickListener가 있을 시 그 내부의 onClick 함수를 실행시켜 주는 방식을 취함을 알 수 있습니다. </p>
<p>추가적으로 performClcik() 함수는 Button의 클릭과 동등한 역할을 하는 함수라고 보면 됩니다.
(TMI. 이 함수를 이용하면 클릭 이벤트를 강제로 발생시킬 수도 있습니다.)</p>
<p>정리하면,
버튼을 클릭하면 내부 로직을 거쳐 performClick() 함수가 실행된다고 봐도 괜찮으며, 만약 OnClickListener가 구현되어 있다면 그 내부의 onClick()을 실행하는 과정을 통해 클릭 이벤트에 대한 처리가 이루어집니다.</p>
<p>코드 플로우를 보아도 아시겠지만, 동작을 처리하기 위해서는 OnClickListener에 대한 구현을 해주어야 합니다. 물론 이 자체가 인터페이스이기 때문에, 사용을 하기 위해서는 이에 대한 구현체를 만들어주는 것이 당연하다고 보여집니다. </p>
<p>View Class에서 봐야할 마지막 함수는 이러한 OnClickListener를 set하도록 도와주는 setOnClickListener() 함수입니다.</p>
<pre><code class="language-java">public void setOnClickListener(@Nullable OnClickListener l) {
    ...
    getListenerInfo().mOnClickListener = l;
}</code></pre>
<p style="color: #4444">@View Class의 setOnClickListener()</p>
<br>

<p>인자값으로 OnClickListener 값을 받습니다. 그리고, 위에서 언급한 ListenerInfo에 그 값을 지정해둡니다. 즉, 클릭 이벤트가 발생할 시, setOnClickListener에서 등록해두었던 OnClickLister를 호출하여 사용하는 것입니다. 또한, OnClickListener에 대한 구현은 이 함수 내부에서 진행해도 괜찮다는 판단을 할 수 있습니다. 그리고, 그러한 구현이 결국 Listenr 객체를 만드는 행위가 됩니다.</p>
<h3 id="3-event-감지-이후-로직의-처리등을-위해-실행되는-callback-함수">3. Event 감지 이후, 로직의 처리등을 위해 실행되는 Callback 함수</h3>
<p>위에 언급한 코드 중 Interface에 존재하는 onClick() 추상메서드가 구체적인 Callback을 담당하는 역할을 할 수 있습니다.</p>
<p>Button에서 클릭 이벤트를 처리할 때는 setOnClickListener 함수를 통해 OnClickListener를 달아주는 것이 우선입니다.</p>
<pre><code class="language-kotlin">testButton.setOnClickListener(object: View.OnClickListener {
    override fun onClick(v: View?) {
        Toast.makeText(this, &quot;버튼이 클릭되었습니다.&quot;, Toast.LENGTH_SHORT).show()
    }
})</code></pre>
<p>OnClickListener Interface에 대한 구현체가 인자값으로 들어갔으며, 그 내부에 onClick() 메서드를 구현했습니다. </p>
<p>앞으로 Button을 클릭하면, View 내부의 performClick() 메서드에서 등록된 Listener의 onClick()을 실행 할 것이며, 그 대상은 testButton에 달아준 Listener의 onClick()이 되는 것입니다. </p>
<p>참고로 위의 코드는 아래와 같은 람다식으로도 표현이 가능합니다.</p>
<pre><code class="language-kotlin">testButton.setOnClickListener { 
    Toast.makeText(this, &quot;버튼이 클릭되었습니다.&quot;, Toast.LENGTH_SHORT).show() 
}</code></pre>
<h3 id="정리">정리</h3>
<p>전체적인 플로우가 어떻게 되는지 정리하고 직접 Listener를 만들어 보도록 하겠습니다.
구분을 위해 <span style="color: blue">View 클래스에서 하는 행동을 파란색</span>, <span style="color: red">Activity에서 하는 행동을 빨간색</span>으로 표시하겠습니다.</p>
<p><strong>준비과정</strong></p>
<ol>
<li><span style="color: blue">OnClickListener Interface를 정의하고, 그 내부에 Callback으로 처리 할 추상 메서드 onClick()을 선언해둡니다.</span></li>
<li><span style="color: blue">Listener를 달아주는 setOnClcikListener() 함수를 선언합니다. 이 함수는 OnClickListener의 구현체를 매개값으로 받아, 그 객체의 내부 함수를 사용할 수 있도록 도와줍니다.</span></li>
<li><span style="color: blue">받은 OnClickListener 객체를 변수로 저장해두고, 동작을 원하는 지점에 onClick()을 사용합니다.</span></li>
<li><span style="color: red">클릭 이벤트를 처리할 Button에 setOnClickListener() 함수를 사용하여 Listener를 달아줍니다. (인터페이스 구현 작업)</span></li>
<li><span style="color: red">이 때, Callback으로 받을 onClick() 함수에 대한 구현까지 완료합니다.</span> </li>
</ol>
<p>** 실행과정 **</p>
<ol>
<li><span style="color: red">Button을 클릭합니다.</span></li>
<li><span style="color: blue">클릭 이벤트를 감지하고, Listener로 미리 받아두었던 객체의 onClick() 메서드를 실행합니다. (위의 performClick()의 역할)</span></li>
</ol>
<h1 id="직접-callback과-listener를-구현해보자">직접 Callback과 Listener를 구현해보자</h1>
<p><code>Button의 Click Event 처리</code> 로직을 그대로 따라가면, Callback과 Listener를 직접 구현 해 볼 수 있을 것 같습니다.</p>
<blockquote>
<p>아래는 이해를 돕기 위한 예제입니다.</p>
</blockquote>
<p><strong>❗ 다음과 같은 상황을 가정해봅니다.</strong>
CreateUserActivity내부에 값을 입력받는 EditText 2개와 값을 선택 가능한 RecyclerView 1개가 존재합니다.
EditText에는 각각 &#39;이름&#39;과 &#39;아이디&#39;를 입력받습니다.
RecycerView에는 &#39;나이&#39;를 선택할 수 있도록 1~100 까지의 숫자를 띄워줍니다.</p>
<p>&#39;나이&#39;를 선택하자마자 회원가입이 진행되고, 새로운 MainActivity로 이동하여 가입한 회원의 정보를 TextView로 띄워주는 프로그램을 만들어 볼 생각입니다.
<br>
<br></p>
<p>여기서의 핵심은 <strong>&#39;나이&#39;를 선택하자마자 발생하는 이벤트</strong> 입니다.
&#39;나이&#39; 데이터는 RecyclerView에 나열되어 있으며, Activity 단에서는 접근이 불가능한 정보가 됩니다. (Activity에서 등록된 Adapter의 내부 값을 가져올 수 없기 때문입니다.)
또한, RecyclerView 내의 항목을 클릭했을 때, 어떠한 참조도 없는 Activity에 대한 화면 전환을 요구한다는 점도 눈여겨 볼 만 합니다.</p>
<p>이러한 경우에, Listener와 Callback을 이용하면 좋겠다는 생각이 듭니다.
CreateUserActivity에서 Activity내의 변수나 컨텍스트등을 사용할 수 있게끔 Listener를 달아주고, callback 메서드까지 구현해줍니다.
이후, RecyclerView Adapter 내부에서 &#39;나이&#39;데이터를 클릭했을 경우 받아온 Listener객체의 callback 메서드를 실행해주면 됩니다.</p>
<p>즉, 앞서 버튼을 예로 든 상황을 고려했을 때,
CreateUserActivity가 MainActivity(Button이 존재하는)역할을,
Adapter Class가 View Class 역할을 하도록 구현하면 됩니다.</p>
<p>진행과정을 코드로 보겠습니다.</p>
<h3 id="1-listener-interface-정의-및-내부-추상-메서드-선언">1. Listener Interface 정의 및 내부 추상 메서드 선언</h3>
<p>&#39;나이&#39; 값을 선택하는 순간을 포착하고, 그 순간 로직을 처리하기를 기대합니다.</p>
<p>이를 요구하는 Listener를 아래와 같이 Interface로 제작 가능합니다.</p>
<pre><code class="language-kotlin">interface AgeSelectedListener {
    fun onAgeSelected(age: Int)
}</code></pre>
<p>내부에는, 감지가 된 순간 실행될 로직을 담을 onAgeSeleted(@param) 추상메서드를 담았습니다. 이 때, CreateUserActivity에는 선택된 &#39;나이&#39;값이 전달 되어야 하므로 age를 인자값으로 받을 수 있도록 하였습니다.</p>
<p>본 Interface는 Adapter class 내부에 선언해도 상관없으며, 마찬가지로 외부에 선언해도 괜찮습니다.</p>
<h3 id="2-listener의-구현체를-매개값으로-받는-부분-만들기">2. Listener의 구현체를 매개값으로 받는 부분 만들기</h3>
<p>View 클래스의 setOnClickListener(OnClickListener l) 을 담당하는 부분입니다.</p>
<p>본 메서드를 직접 만들어, Adapter 전체에서 사용 할 listener 객체를 받아올 수 있지만,
굳이 메서드를 만들지 않고, 생성자를 통해 listener 객체를 받아오도록 하겠습니다.</p>
<p>선언한 클래스에 생성자를 아래와 같이 받아옵니다.</p>
<pre><code class="language-kotlin">class AgeAdapter(private val listener: AgeSelectedListener) : 
RecyclerView.Adapter&lt;AgeAdapter.AgeViewHolder&gt;() {
    override fun onCreateViewHolder(
    ...
</code></pre>
<p>Adapter Class를 만들 때 Listener 객체를 넘겨주어 사용할 수 있습니다.</p>
<h3 id="3-받은-listener를-변수로-저장해두고-원하는-지점에-callback-함수-호출">3. 받은 Listener를 변수로 저장해두고, 원하는 지점에 Callback 함수 호출</h3>
<p>여기서는 생성자로 넘어온 Listener를 굳이 새로운 변수로 저장하여 사용 할 필요가 없습니다.
생성자에서 정의한 <code>listener</code> 변수를 이용하면 되기 때문입니다.</p>
<p>이 <code>listener</code> 변수를 이용하여 내부의 onAgeSelected(age: Int)를 호출하여 사용하면 됩니다.</p>
<p>각 &#39;나이&#39;항목의 뷰를 클릭하는 경우에 동작하기를 원하므로,
만들어준 ViewHolder의 각 itemView를 클릭하는 경우 동작하도록 구현하겠습니다.</p>
<pre><code class="language-kotlin">override fun getItemCount(): Int = 100

override fun onBindViewHolder(holder: AgeViewHolder, position: Int) {
    val age = position + 1    // 나이는 1~100 (position이 0~99인 관계로)

    // xml상에서 나이를 보여주는 TextView의 변수명을 textView로 정했음
    holder.textView.text = age.toString() 

    holder.itemView.setOnClickListener {
        listener.onAgeSelected(age)
    }
}</code></pre>
<p>핵심은 onAgeSelected(age) 를 사용한 부분입니다.
그 외 코드에 대한 설명은 주석 설명으로 대체합니다. 아래에 제공하는 코드를 보시면 이해가 되실겁니다.</p>
<h3 id="4-createuseractivity에서-listener-달아주기-인터페이스-구현">4. CreateUserActivity에서 Listener 달아주기 (인터페이스 구현)</h3>
<p>아직 CreateUserActivity와 Adatper간 연결이 이루어지지 않았습니다.
이는 AgeAdapter 클래스의 생성자에 정의된 AgeSelectedListener 객체를 구현하는 과정으로 이루어 질 것입니다.</p>
<p>아래와 같이 CreateUserActivity 자체를 구현 클래스로 설정할 수 있습니다.</p>
<pre><code class="language-kotlin">class CreateUserActivity : AppCompatActivity(), AgeSelectedListener {
...
    override fun onAgeSelected(age: Int) {...}
}
</code></pre>
<p>이러한 방법을 택했을 경우, 클래스 자체를 생성자의 값으로 넘겨주면 되므로, 아래와 같이 adapter를 달아줄 수 있습니다.</p>
<pre><code class="language-kotlin">recycler.adapter = AgeAdapter(this)</code></pre>
<p>혹은, 람다식을 사용 할 경우, AgeAdapter를 생성함과 동시에 익명 객체를 만들어주는 방식으로 구현이 가능합니다. 아래와 같은 작업을 통해 이루어집니다.</p>
<pre><code class="language-kotlin">recycler.adapter = AgeAdapter(object: AgeSelectedListener {
    override fun onAgeSelected(age: Int) {...}
})</code></pre>
<p>두 번째 방법을 사용했을 경우, CreateUserActivity에서 AgeSelectedListener Interface를 직접 구현 할 필요가 없어집니다.</p>
<h3 id="5-callback-함수의-구현">5. Callback 함수의 구현</h3>
<p>onAgeSelected(age: Int) 함수의 내부를 구현합니다.</p>
<p>EditText에 입력된 사용자의 &#39;이름&#39;과 &#39;아이디&#39; 값,
RecyclerView에서 선택되어 age라는 값으로 넘어온 &#39;나이&#39; 를
Intent를 사용하여 MainActivity로 넘겨줍니다.</p>
<pre><code class="language-kotlin">override fun onAgeSelected(age: Int) {
    val intent = Intent(this, MainActivity::class.java).apply {
        putExtra(&quot;USER_NAME&quot;, input_name.text.toString())
        putExtra(&quot;USER_ID&quot;, input_id.text.toString())
        putExtra(&quot;USER_AGE&quot;, age.toString())
    }
    startActivity(intent)
}</code></pre>
<h1 id="실행-결과">실행 결과</h1>
<img src="https://images.velog.io/images/dev_2dong/post/5b072281-b0e4-4bc6-beca-2e751951b9f6/200913_01.jpg" style="width: 300px"/>

<p>CreateUserActivity에서 &#39;아이디&#39;와 &#39;이름&#39;을 입력하고, &#39;나이&#39;를 클릭하면, 아래의 MainActivity로 정보를 전달하여 화면에 띄워주는 과정입니다.</p>
<img src="https://images.velog.io/images/dev_2dong/post/8d8a0d9f-dd64-4cf4-a064-a3b3b3e757c4/200914_02.jpg" style="width: 300px"/>

<h1 id="전체-코드">전체 코드</h1>
<p>strings.xml</p>
<pre><code class="language-xml">&lt;resources&gt;
    &lt;string name=&quot;app_name&quot;&gt;CallbackExample&lt;/string&gt;
    &lt;string name=&quot;result&quot;&gt;안녕하세요 name회원님!!\n아이디는 id입니다.\n나이는 age입니다.\n환영합니다!!&lt;/string&gt;
&lt;/resources&gt;</code></pre>
<p><br><br><br></p>
<p>activity_create_user.xml</p>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;LinearLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;
    android:padding=&quot;24dp&quot;
    android:orientation=&quot;vertical&quot;
    tools:context=&quot;.CreateUserActivity&quot;&gt;

    &lt;TextView
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:text=&quot;아이디&quot; /&gt;

    &lt;EditText
        android:id=&quot;@+id/input_id&quot;
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:hint=&quot;아이디를 입력해주세요&quot; /&gt;

    &lt;TextView
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:layout_marginTop=&quot;16dp&quot;
        android:text=&quot;이름&quot; /&gt;

    &lt;EditText
        android:id=&quot;@+id/input_name&quot;
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:hint=&quot;이름을 입력해주세요&quot; /&gt;

    &lt;TextView
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:layout_marginTop=&quot;16dp&quot;
        android:text=&quot;나이 (클릭시 회원가입이 바로 진행됩니다.)&quot; /&gt;

    &lt;androidx.recyclerview.widget.RecyclerView
        android:id=&quot;@+id/recycler&quot;
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:layout_marginTop=&quot;8dp&quot; /&gt;

&lt;/LinearLayout&gt;</code></pre>
<p><br><br><br></p>
<p>activity_main.xml</p>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;LinearLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;
    android:padding=&quot;24dp&quot;
    android:orientation=&quot;vertical&quot;
    tools:context=&quot;.MainActivity&quot;&gt;

    &lt;TextView
        android:id=&quot;@+id/result&quot;
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:textSize=&quot;22sp&quot;
        android:text=&quot;@string/result&quot;
        android:lineSpacingMultiplier=&quot;1.5&quot;/&gt;

&lt;/LinearLayout&gt;</code></pre>
<p><br><br><br></p>
<p>CreateUserActivity</p>
<pre><code class="language-kotlin">class CreateUserActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_create_user)

        recycler.layoutManager = LinearLayoutManager(this)
        recycler.adapter = AgeAdapter(object: AgeSelectedListener {
            override fun onAgeSelected(age: Int) {
                val intent = Intent(this@CreateUserActivity, MainActivity::class.java).apply {
                    putExtra(&quot;USER_NAME&quot;, input_name.text.toString())
                    putExtra(&quot;USER_ID&quot;, input_id.text.toString())
                    putExtra(&quot;USER_AGE&quot;, age.toString())
                }

                startActivity(intent)
            }
        })
    }
}</code></pre>
<p><br><br><br>
MainActivity</p>
<pre><code class="language-kotlin">class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val name = intent.getStringExtra(&quot;USER_NAME&quot;)!!
        val id = intent.getStringExtra(&quot;USER_ID&quot;)!!
        val age = intent.getStringExtra(&quot;USER_AGE&quot;)!!

        result.text = resources.getString(R.string.result)
            .replace(&quot;name&quot;, name)
            .replace(&quot;id&quot;, id)
            .replace(&quot;age&quot;, age)
    }
}</code></pre>
<p><br><br><br>
AgeAdapter / AgeSelectedInterface</p>
<pre><code class="language-kotlin">class AgeAdapter(private val listener: AgeSelectedListener) : RecyclerView.Adapter&lt;AgeAdapter.AgeViewHolder&gt;() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AgeViewHolder {
        return AgeViewHolder(LayoutInflater.from(parent.context).inflate(android.R.layout.simple_list_item_1, parent, false))
    }

    override fun getItemCount(): Int = 100

    override fun onBindViewHolder(holder: AgeViewHolder, position: Int) {
        val age = position + 1
        holder.textView.text = age.toString()
        holder.itemView.setOnClickListener {
            listener.onAgeSelected(age)
        }
    }

    inner class AgeViewHolder(view: View): RecyclerView.ViewHolder(view) {
        val textView = view.findViewById&lt;TextView&gt;(android.R.id.text1)
    }
}

interface AgeSelectedListener {
    fun onAgeSelected(age: Int)
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android Clone App] 당근마켓(#006) 회원가입 기능 구현]]></title>
            <link>https://velog.io/@dev_2dong/Android-Clone-App-%EB%8B%B9%EA%B7%BC%EB%A7%88%EC%BC%93006-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@dev_2dong/Android-Clone-App-%EB%8B%B9%EA%B7%BC%EB%A7%88%EC%BC%93006-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Fri, 11 Sep 2020 07:59:27 GMT</pubDate>
            <description><![CDATA[<p>며칠간 클론앱 구현에 관련된 포스팅을 안했더니,
구현은 하면서도 자꾸 글 작성을 미루고 있다는것이 정말 신경 많이 쓰였다 ㅠㅠ</p>
<p>그래서 여태껏 했던 작업을 정리하는겸 글을 써본다.</p>
<h1 id="012--지역-검색-기능">012 : 지역 검색 기능</h1>
<p>우선적으로 구현을 했던 기능이 지역 검색 기능이었다.</p>
<p>Room Database에 들어있는 지역정보를 쿼리문 돌려서 검색하도록 만들어준 기능이다.
검색어를 입력할 때 마다, DB에 포함되는 단어가 있는지를 보여주고 RecyclerView에 뿌려주는 식으로 구현을 했다.</p>
<p>나는 생각보다 컴퓨터의 능력에 의구심을 많이 가지는 타입인데,
지금은 데이터가 얼마 없기 때문에, 처리 속도가 어느정도 보장 됐다고 생각한다.
만약, 데이터가 무수히 많을 경우에는 어떻게 할까?
그리고, 그 데이터가 DB가 아닌 네트워크에서의 처리라면 어떻게 해야할까?</p>
<p>처음으로 Paging을 사용해보았고, 검색기능을 구현 해보았기 때문에 
이런 부분을 우선적으로 고려하지는 않았지만, 충분히 생각을 해봐야 할 문제같다.</p>
<p><img src="https://images.velog.io/images/dev_2dong/post/d11e9da1-621f-4c8e-b67b-3a6f11138b39/26.JPG" alt=""></p>
<p>검색어로 입력된 단어를 포함하는 데이터를 조회하여 보여준다.
데이터가 없다면 맨 오른쪽 사진처럼 검색 결과가 없다는 문구를 보여준다.</p>
<p>이렇게하니 생긴 또다른 문제는,
한글 같은 경우는 &#39;서동&#39;을 입력 할 경우 &#39;서&#39; -&gt; &#39;서ㄷ&#39; -&gt; &#39;서도&#39; -&gt; &#39;서동&#39; 의 순으로 작성이 되는데,
&#39;서&#39;의 타이밍에서 검색된 결과가 존재 했을 경우
이어서 &#39;서ㄷ&#39;가 되는 즉시 검색 결과가 존재하지 않는 문제가 생기는 것이다.</p>
<p>이게 문제가 뭐냐면.. 글자를 입력할 때 마다 recycerview가 visible / gone 상태가 계속 반복되다보니 어지럽다.. 
보통 어플의 검색기능을 사용하면 저렇게 확확 화면이 바뀌지 않는데,
이러한 알고리즘도 찾아봐야 할 것 같다.</p>
<h1 id="013--닉네임-설정-화면-구성">013 : 닉네임 설정 화면 구성</h1>
<img src="https://images.velog.io/images/dev_2dong/post/aa3d1553-29c8-495a-afd4-b4ab1789e382/27.jpg" style="width: 350px"/>

<p>닉네임 설정 화면은 위와 같이 만들었다.
특별하게 어려운 부분은 없었고, ImageView를 동그랗게 만들기 위해 CircleImageView 라이브러리를 사용했다.</p>
<p>아직, 프로필 사진을 넣는 기능은 별도로 구현하지 않았다.</p>
<h1 id="014--만들어진-화면-연결">014 : 만들어진 화면 연결</h1>
<p>그리고 초기 로그인 화면부터 닉네임 설정, 지역 설정 화면, 메인 화면까지를 쭉 연결해보았다.</p>
<p>플로우는 아래와 같다.
<img src="https://images.velog.io/images/dev_2dong/post/08f2aaa2-99d8-411b-847b-31ce73a6c8c5/28.JPG" alt=""></p>
<p>파이어베이스에서는 로그인 된 유저의 Uid 값을 저장한다.
이를 SharedPreference 값으로 저장하여 어디에서나 데이터 처리에서 사용할 수 있도록 지정해두었다. (로그아웃 시 해제시킨다.)</p>
<p>이렇게 저장된 Uid를 통해, db에 저장된 유저의 정보를 간단하게 조회 할 수 있게 되는 예시가 존재하며 간단하게 구현을 해놓았다.</p>
<img src="https://images.velog.io/images/dev_2dong/post/fafb67ea-959d-4389-a71b-d40b1a1eefd3/29.jpg" style="width: 350px"/>

<h1 id="015--splash-화면-설정">015 : Splash 화면 설정</h1>
<p>간단한 Splash 화면을 만들어놨다.
로그인이 유지가 되어있다면 메인화면으로 바로 이동하고,
사용자가 로그아웃을 시켜 둔 상태라면 로그인 화면으로 이동하도록 설정해두었다.</p>
<img src="https://images.velog.io/images/dev_2dong/post/d2d4eab3-fe4e-48c1-a4ea-005159cab504/30.jpg" style="width: 350px"/>
ㅋㅋㅋ 원래는 이미지 크기를 축소시켜서 화면의 1/4정도만 차지하도록 하려고 했는데, 뭔가 이렇게 뜨는것도 귀엽고 웃겨서.. 이대로 두기로 결정했다.

<p>이미지는 아래 링크에서 다운로드 받아 사용했다.
<a href="https://pngtree.com/">Pngtree.com</a></p>
<hr>
<h1 id="번외--한계-및-처리-해야-할-문제점">번외 : 한계 및 처리 해야 할 문제점</h1>
<p>현재 MainActivity에는 5개의 Fragment가 존재한다. 그리고 각각의 Fragment들이 데이터를 처리하게 될 것이다.
현재 &#39;나의 당근&#39; 탭을 다루는 Fragment에 유저의 정보가 간략하게 뜨게된다.
이는 Firebase의 Database에 저장되어있는 유저의 정보를 Uid를 통해 접근하여 데이터를 받아와 띄워주는 형태이다.</p>
<p>원했던 설계는, MainActivity에 접근하자마자 Firebase상의 Database에 접근하여 User의 정보를 가져오는건데, 지금은 &#39;나의 당근&#39; Fragment에 들어가야 그제서야 Firebase와 연결을 하게된다. 그러다보니 위의 스크린샷처럼 바로 데이터를 띄우는 것이 아니라, 약간의 시간이 지나고서야 데이터를 뷰에 보여주는 식이다.</p>
<p>사실, 이러한 데이터를 바로 띄우는 방법으로 2가지 정도가 더 생각난다.
그 중 하나는 이미 실천을 해보았다. MainActivity에서 쓸모없는 User의 정보를 받아오는 방법인데, MainActivity 자체에서는 사용하지도 않을 데이터를 굳이 여기서 받아와야하나 하는 생각에 그만두었다. (데이터는 최대한 빨리 띄워지긴 한다.)
남은 하나는 내장DB에 이러한 정보들을 저장해 두는 것이다. 사실 보안에 대해서는 1도 신경 안쓰는 프로젝트기는 하지만, User의 정보를 계속해서 내장DB 혹은 SharedPreference에 넣는것은 바람직하지 못하다고 생각하며, 그러한 도구에 자꾸 의존하게 될 것 같아서 사용을 줄이기로 결정했다.</p>
<p>최대한의 ViewModel과 Firebase간의 연동 작업을 통해, 그리고 이를 통해서 ViewModel을 조금이나마 더 잘 활용할 수 있도록 만드는 것이 더 큰 목표이기 때문에, ViewModel을 사용하는 방향으로 계속해서 고민해서 문제를 해결해 보도록 하겠다.</p>
<p>그리고, 사실 이 프로젝트를 시작하기 전에는 몰랐는데,
뭔가 조금씩 보안쪽에 신경을 쓰면서 작업을 해야겠다는 생각이 들었다.
주변에서 들리는 정보도 많고, 아예 신경을 안 쓸 수 없는 부분도 아닌것이.. 충분히 어려운 내용임을 인지하고 있고 분명 회사에서 프로젝트를 하면서도 계속해서 어려움을 겪고 사용할 것이기 때문이다.</p>
<p>물론 이번 프로젝트에서는 보안따위 신경쓰지 않지만, 플젝이 끝나고나면 바로 기본적인 보안 서적을 들고 공부 할 계획에 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android Clone App] 당근마켓(#005) 지역 선택 화면 만들기(Room, Paging)]]></title>
            <link>https://velog.io/@dev_2dong/Android-Clone-App-%EB%8B%B9%EA%B7%BC%EB%A7%88%EC%BC%93005-%EC%A7%80%EC%97%AD-%EC%84%A0%ED%83%9D-%ED%99%94%EB%A9%B4-%EB%A7%8C%EB%93%A4%EA%B8%B0Room-Paging</link>
            <guid>https://velog.io/@dev_2dong/Android-Clone-App-%EB%8B%B9%EA%B7%BC%EB%A7%88%EC%BC%93005-%EC%A7%80%EC%97%AD-%EC%84%A0%ED%83%9D-%ED%99%94%EB%A9%B4-%EB%A7%8C%EB%93%A4%EA%B8%B0Room-Paging</guid>
            <pubDate>Wed, 02 Sep 2020 15:37:23 GMT</pubDate>
            <description><![CDATA[<img src="https://images.velog.io/images/dev_2dong/post/d9c6d224-5404-4e69-a887-b60982238549/21.jpg" style="width: 350px"/>

<p>(위 사진의 출처 : 당근마켓)</p>
<p>다음으로는 <code>지역 정보</code>를 선택하는 화면을 만들기로 결정했다.
아무래도 지역 기반 앱이기 때문에, 모든 기능에 지역에 관련된 값이 인수로 사용되는 경우가 많기 때문이다.</p>
<h1 id="010--지역-정보-room-database-삽입">010 : 지역 정보 Room Database 삽입</h1>
<p>간단한 지역 정보를 만들어 넣어주었다.</p>
<p>데이터는 JSON 형식의 파일로 제공하며,
값은 id, name, latitude, longitude로 구성되어있다. </p>
<p>Room DB가 만들어 질 때 최초 1회 삽입 되도록 구현을 했다. (Work 사용)</p>
<p>Dao, Repository, Database 등이 만들어 지기 때문에, 
앞으로의 재활용성 향상을 위해 Koin을 사용하기로 결정했다.
만들기는 했는데 어떻게 사용하는지 도무지 모르겠어서.. 우선은 일반적인 사용방법으로 Room을 사용했다. (여기서 정말 많은 시간을 쏟은듯..)</p>
<h1 id="011--지역-정보-리스트-값-뿌려주기">011 : 지역 정보 리스트 값 뿌려주기</h1>
<p>지명 정보는 전국적으로 보았을 때 적은 양의 데이터가 아닐 것이다.
만약 이 정보를 한꺼번에 불러와서 recyclerview에 뿌려주려고 한다면 당연 버벅임이 생길 것이다.</p>
<p>실제로 당근마켓 지역 리스트는 일정 갯수의 지명 데이터만 보여준 뒤, 스크롤을 일정 부분 내렸을 경우 추가적으로 데이터를 뷰에 업데이트 해주는 방식을 취한다.
(이 방식은 당근마켓 뿐 아니라 각종 SNS, 전화번호부 등에서도 사용한다.)</p>
<p>이번에 구현을 해보고 싶었던 부분이다.</p>
<p><strong>최초에 일부의 데이터만 꺼내와서 화면에 보여주고, 일정 스크롤을 내렸을 경우 추가적으로 데이터를 불러와서 화면에 뿌려주기!</strong></p>
<p>예전에는 이런 기능까지 각종 이벤트 탐지 리스너를 사용해서 직접 구현했었어야 했다고 하지만.. 지금은 Paging Library를 사용하여 간단하게 구현할 수 있다고 한다.</p>
<p>더군다나 Paging은 내가 사용하고 있는 Room 데이터베이스에 대한 지원이 확실하다.</p>
<p>그래서 바로 인터넷을 보면서 공부하기 시작했다.
꽤나 많은 자료들이 있었고, 충분히 이해 한 다음! 적용을 해보았다.
(조만간 Paging에 대해 포스팅을 해보는 시간을 가지는 것도 좋겠다.)</p>
<pre><code class="language-kotlin">class RegionListViewModel(
    application: Application
): AndroidViewModel(application) {
    private var allRegionsLiveData: LiveData&lt;PagedList&lt;Region&gt;&gt;

    init {
        val config = PagedList.Config.Builder()
            .setEnablePlaceholders(false)
            .setPageSize(16)
            .build()

        val factory: DataSource.Factory&lt;Int, Region&gt; =
            AppDatabase.getInstance(getApplication()).regionDao().selectAllRegions()

        val pagedListBuilder: LivePagedListBuilder&lt;Int, Region&gt; =
            LivePagedListBuilder&lt;Int, Region&gt;(factory, config)

        allRegionsLiveData = pagedListBuilder.build()
    }

    fun getRegionsLiveData() = allRegionsLiveData</code></pre>
<p>처음에 전체 데이터가 한 번에 load 되어 recyclerview에 뿌려졌다.
왜 그런지 구글링을 열심히 하다가.. Config에서 setEnablePlaceholders 속성을 false로 만들어주어 데이터를 연이어 받아오는 작업을 막아주어야 한다는 것을 알았다.</p>
<p>그 이후 PagedList 객체가 페이징 동작을 수행하는 방식이다.</p>
<img src="https://images.velog.io/images/dev_2dong/post/81d30fa1-5285-4de8-92f8-0d11ef139e2f/22.jpg" style="width: 350px"/>

<p>확인은 스크롤바를 통해 가능했다.</p>
<p>사진에는 스크롤바가 보이지 않지만, 처음에는 스크롤바가 엄청 컸다.
스크롤을 내리면서 옆의 스크롤바가 점점 작아지는 것을 확인했는데, 이를 통해 데이터가 계속해서 로딩 되고 뷰가 업데이트 됐음을 확인할 수 있겠다.</p>
<hr>
<p>다음에는 UI를 조금 더 예쁘게 꾸미고, 검색 기능까지 구현을 해보도록 하겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android Clone App] 당근마켓(#004) BottomNavigation + Toolbar]]></title>
            <link>https://velog.io/@dev_2dong/Android-Clone-App-%EB%8B%B9%EA%B7%BC%EB%A7%88%EC%BC%93004-BottomNavigation-Toolbar</link>
            <guid>https://velog.io/@dev_2dong/Android-Clone-App-%EB%8B%B9%EA%B7%BC%EB%A7%88%EC%BC%93004-BottomNavigation-Toolbar</guid>
            <pubDate>Mon, 31 Aug 2020 11:13:42 GMT</pubDate>
            <description><![CDATA[<h1 id="007--프로젝트-제작-재료-준비">007 : 프로젝트 제작 재료 준비</h1>
<p>예전에 Dimo 유튜브 채널에서 이런 뉘앙스의 말을 했던것을 기억했다.</p>
<blockquote>
<p>프로젝트에서 코딩작업을 시작하기 전에 사용할 라이브러리 조사를 미리 해놓고, 충분히 자료 수집과 조사가 된 상태에서 코딩 작업을 시작하라.</p>
</blockquote>
<p>이유인즉, 코딩 할 때는 오로지 코딩에만 집중할 수 있기 위해서라고 했다.</p>
<p>작업을 열심히 하고있는데 갑자기 이미지파일 다운받는다고 서핑하고.. 라이브러리 필요하다고 또 떠나버리고 그러면 코딩 효율이 많이 떨어진다는 이유에서다.</p>
<p>이번 프로젝트에서 필요한 리소스들을 준비 해놓겠다는 생각으로 진행했기 때문에 어제 (주말) 기분 전환(?)겸 준비를 해보았다. </p>
<ul>
<li>각종 이미지파일</li>
<li>사용할 라이브러리</li>
<li>margin / padding / text size의 dimens 설정</li>
<li>color</li>
<li>font</li>
</ul>
<p>근데 막상 작업한 결과를 보니 폰트가 이쁘지는 않네.. 조금 더 개발이 진행되고나면 폰트도 더 자연스러운걸로 바꿔봐야겠다.
이미지도 그렇게 자연스럽게 녹아들지가 않는다. 역시 디자이너가 필요한건가?</p>
<h1 id="008--bottomnavigation">008 : BottomNavigation</h1>
<p>당근마켓에서는 BottomNavigation을 통해 일차적으로 탭 전환을 하도록 설계 되어있다.
최대한 비슷하게끔 구현을 하도록 노력을 했고, 각 탭을 누를 때 마다 Fragment가 전환되도록 간단한 구현까지 해놓았다.</p>
<h1 id="009--toolbar">009 : Toolbar</h1>
<p>Toolbar를 구현함에 있어, 어떤 방식으로 Toolbar를 Screen에 띄울까에 대한 부분의 고민을 상당히 많이 했다. 내 실력으로 제안할 수 있는 방법의 수는 2가지였다.</p>
<ol>
<li>Activity 내에서 Toolbar를 만들고, 
각 Fragment에서 Title, Image만 바꾸는 식으로 View를 재활용 하는 방법</li>
<li>각 Fragment마다 자신의 Toolbar를 각각 만드는 방법</li>
</ol>
<p>재활용적인 측면을 생각해보고, xml 코드를 짧게 줄일 수 있는 방법으로는 1번이 좋아보였다. </p>
<p>하지만, 1번 방법을 사용하면 발생할 수 있는 문제들이 있었다.</p>
<ul>
<li>xml 코드가 짧아지는 대신 fragment 영역에서의 코드가 조금 더 더러워 질 수 있겠다.</li>
<li>후에 HomeFragment에서만 Collapsing Toolbar를 사용할 예정(다른 Fragment에서는 사용하지 않는다.)이기 때문에, Activity에서 처리하면 이러한 처리가 까다로워 질 수 도 있겠다는 생각.</li>
<li>상품의 상세 화면을 볼 때, 새로운 Fragment가 Toolbar까지 덮어버려야 하는데, Activity단에 Toolbar가 존재한다면 새로운 Fragment가 그 Toolbar를 덮는 애니메이션이 자연스러울 것 같지 않다는 생각..? (말로 설명하니 어렵다..)</li>
</ul>
<p>그래서 2번 방법을 택했다.</p>
<p>최종적으로는 아래의 결과물이 나왔다.
<img src="https://images.velog.io/images/dev_2dong/post/bcea4be8-0c47-4bd1-b3e1-a23b3a537cf0/20.JPG" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android Clone App] 당근마켓(#003) 데이터 관리 툴 변경]]></title>
            <link>https://velog.io/@dev_2dong/Android-Clone-App-%EB%8B%B9%EA%B7%BC%EB%A7%88%EC%BC%93003-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EA%B4%80%EB%A6%AC-%ED%88%B4-%EB%B3%80%EA%B2%BD</link>
            <guid>https://velog.io/@dev_2dong/Android-Clone-App-%EB%8B%B9%EA%B7%BC%EB%A7%88%EC%BC%93003-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EA%B4%80%EB%A6%AC-%ED%88%B4-%EB%B3%80%EA%B2%BD</guid>
            <pubDate>Sat, 29 Aug 2020 09:32:45 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>일차 방식으로 포스팅을 하려고 했는데, 작업 단위를 일차로 구분하기 모호합니다.
따라서 포스팅 제목 형식을 바꾸었습니다.</p>
</blockquote>
<h1 id="005--django-사용-포기">005 : Django 사용 포기</h1>
<p>어떻게든 서버를 이용하여 프로젝트를 진행해보려고 했지만..
내가 원하는 수준까지 서버를 구현하려면 공부를 해야 할 부분이 생각보다 많았다.
(간단할 것 같다고 얕잡아(봤던 건 아니지만..) 보았던 나에게 마음속으로 욕하는중..)</p>
<p>5일동안 인터넷에서 여러 자료를 찾아보며
실제로 Local에서만 동작하는 REST API를 만들어 보기도 했고, 
이를 기반으로 샘플 테스트 앱을 만들어서 실행까지 해보았다.</p>
<p>하지만, 말 그대로 간단한 Field 값들에 대해서만 CRUD 기능정도 테스트가 가능했는데,
앞으로 내가 구현할 막대한 기능들을 생각해보면
진행할 프로젝트에서는 이러한 테스트로는 어림도 없는 수준이 될 것이 분명했다.</p>
<p>서버에 중점을 둘 프로젝트가 아니고
(단지 Retrofit을 사용해서 구현을 하고 싶었고, Firebase를 사용하기 싫었을 뿐이다..)
앞으로 구현하면서 스트레스를 분명 받겠지만 그 원인이 서버단 코드 때문이기를 원하지 않기 때문에 Django 사용을 멈추고 (경험이라고 받아들이기엔 아까운 나의 지난 5일 ㅠㅠ) 데이터 관리를 다른 방식으로 하기로 결정했다.</p>
<p>그럼 어떤 관리 툴을 사용 할 것이냐?
직접 구현하는 서버를 이용하지 않는다면.. 내가 아는 선에서 선택지는 얼마 없는 것 같다.
결국 절대 사용하지 않겠다고 다짐했던 <strong>Firebase Cloud Firestore</strong>를 사용하기로 결정했다!</p>
<p>이를 사용하기로 결정한 이유는 아래와 같다.</p>
<ol>
<li>이전에 실패했던 경험이 있는데, 설마 같은 실수를 반복할까 싶어서</li>
<li>사진을 저장하는 저장소를 Firebase Storage를 사용하기로 결정했기 때문</li>
<li>다른 툴에 비해, 미래를 보았을 때 Firebase 사용법 공부를 해 놓으면 큰 도움이 될 것 같아서</li>
</ol>
<p>대신.. 정말 데이터 구조를 잘 짜고 들어가야 함은 확실하고
쿼리문 적용을 잘 할 수 있도록.. 어떤 기능을 지원하는지 문서를 쭉 훑어보고 시작할 것이다.</p>
<h1 id="006--회원가입-및-로그인-기능-구현">006 : 회원가입 및 로그인 기능 구현</h1>
<p>당근마켓 실제 앱에서 로그인 및 회원가입을 진행하는 방식은 다음과 같다.
휴대폰 번호 인증을 SMS를 통해 진행하고, 이미 등록된 휴대폰 번호 일 경우 즉시 로그인. 아닐 경우 회원 등록을 이어서 진행하는 방식이다.</p>
<p>우선 SMS 서비스를 이용하는 것은 돈이 필요하다. 그래서 지난 번 프로젝트를 진행했을 때는 형식적으로 뿐이 구현을 못했는데.. 가령 앱 자체에서 4자리 번호를 랜덤 생성하고, 생성된 번호를 입력해서 인증을 받는 방식으로 구현했었다. </p>
<p>본 프로젝트에서, 아마 UI적으로 유일하게 다르게 될 부분이 바로 회원등록 및 로그인 부분이 될 것이다. 애초에 회원 등록이 되지 않은 사용자는 메인 화면 진입이 안되도록 최초 설계 할 계획이다. (실제 당근마켓에서는 비회원도 지역설정만 한다면, 메인 화면으로 진입 가능하도록 만들어져있다.)</p>
<p>우선 비회원이 메인 화면에서 여러 작업을 할 경우, 이를 처리하는 로직이 매우 복잡해짐을 이전 프로젝트에서 경험을 했었기 때문인 이유가 있다. 다음 이유로는, 완성된 프로젝트에 기능적인 요소를 더해보는 연습을 해보고 싶기 때문이다.</p>
<p>최초에는 기능을 최소화하고, 후에 살을 붙여나가는 식으로 프로젝트를 꾸려나가고 싶은 마음이 크기 때문인데, 처음부터 모든 기능을 다 구현하려고 했다가 거의 대부분의 프로젝트들이 제대로 된 완성품도 없이 에러만 가득한 상태로 끝나버렸기 때문이다. 이번엔 적어도 제대로 돌아가는 앱이라도 만들어보고 싶다.</p>
<p>사설이 엄청 길었는데
결국은 회원가입 및 로그인 화면을 구현했다.</p>
<img src="https://images.velog.io/images/dev_2dong/post/5c6e5e38-087a-41a1-9ad2-2987b34089eb/10.jpg" style="width:350px"/>
<img src="https://images.velog.io/images/dev_2dong/post/0560fb01-8449-4ae2-8e9d-ccb09a6ed3b6/14.jpg" style="width:350px"/>
로그인 화면에 들어선 다음 자신의 아이디와 비밀번호를 입력해주면
Firebase Auth에서 등록된 계정 정보를 확인해 준 뒤, 일치하는 정보라면 위와같이 다음 화면으로 이동시켜 준다.

<p>물론 비밀번호가 틀렸거나, 등록되지 않은 계정 정보가 존재할 수도 있으므로 각각의 경우를 분기 처리했다. </p>
<img src="https://images.velog.io/images/dev_2dong/post/f1f06bfe-b77b-4b9b-b57d-07f1823c80a6/11.jpg" style="width:350px"/>
<img src="https://images.velog.io/images/dev_2dong/post/5d7c98be-8257-429a-8f5a-f7b77463a266/12.jpg" style="width:350px"/>

<p>아래의 경우가 등록되지 않은 계정일 경우인데, 예를 누르면 회원의 정보를 자세히 입력할 수 있는 창으로 넘어간다.</p>
<p>회원 가입이 완료됐다면 회원이 등록했던 상세정보(지역, 닉네임 등)를 FireStore에 추가적으로 저장했다. 이 때, auth에서 제공하는 uid를 field 값으로 저장해서 PK 느낌으로 만들어놨다.
(지금 생각해보니 uid를 field 값으로 저장하는게 아니라 document의 이름으로 저장해도 괜찮을 것 같다는 생각이든다. 조금 더 알아봐야겠다.)</p>
<hr>
<h1 id="마무리">마무리</h1>
<p>로그인 화면과 회원가입 화면은 임의로 내가 만들었기 때문에 당근마켓의 UI를 따라 갈 방법이 없다. 앞으로 당근마켓의 디자인 스타일을 파악해가며 그럴싸하게 만드는 방법 뿐이 없는 것 같다.</p>
<p>아 그리고, 비밀번호 부분에 마스킹을 안해뒀는데 이것도 처리해야한다 ㅋㅋ 할려고 했다가 까먹었는데 사진 업로드 하면서 알았다..</p>
<p>어쨌든 오늘 하루종일 Server를 어떻게 해야할까 고민이 많았는데 (오늘 뿐 아니라 5일 정도동안..)
그래도 Firebase 쓰기로 결정해서 그런지 마음이 매우 편해졌고.. 앞으로 진행을 슥슥 할 수 있지 않을까 기대를 해본다! </p>
<p>내일은 사용 할 각종 이미지 파일을 준비하고 사용 할 라이브러리를 미리 알아봐야겠다.</p>
<p>오늘 남은 시간은 FireStore 문서를 읽어보며 어떤식으로 데이터 구조를 짤지 생각을 더 해봐야겠다.</p>
<p>끝!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android Clone App] 당근마켓 (2일차) 서버 구현 작업 테스트]]></title>
            <link>https://velog.io/@dev_2dong/Android-Clone-App-%EB%8B%B9%EA%B7%BC%EB%A7%88%EC%BC%93-2%EC%9D%BC%EC%B0%A8-%EC%84%9C%EB%B2%84-%EA%B5%AC%ED%98%84-%EC%9E%91%EC%97%85-%ED%85%8C%EC%8A%A4%ED%8A%B8</link>
            <guid>https://velog.io/@dev_2dong/Android-Clone-App-%EB%8B%B9%EA%B7%BC%EB%A7%88%EC%BC%93-2%EC%9D%BC%EC%B0%A8-%EC%84%9C%EB%B2%84-%EA%B5%AC%ED%98%84-%EC%9E%91%EC%97%85-%ED%85%8C%EC%8A%A4%ED%8A%B8</guid>
            <pubDate>Tue, 25 Aug 2020 15:05:56 GMT</pubDate>
            <description><![CDATA[<h1 id="004--user-model-구현-중">004 : User Model 구현 중</h1>
<p>원래 오늘 하기로 계획 세워둔 것들이 있었는데, 하루종일 서버 작업 한다고 하나도 못했다 ㅎㅎ.
음.. 그렇다면 서버 작업이 어느정도 이뤄져야 했을텐데 그것도 아니다.</p>
<p>MongoDB 사용법부터 헤맸고.. 이를 Django와 연결하는 부분도 엉망..
기본적인 데이터 추가를 구현하려고 했지만 결국 500 Server Error를 뽑아내고 종료! ㅋㅋ</p>
<p>기본 베이스 없이 서버 설계하려고 하니 막막하다. (심지어 파이썬 언어도 사용할 줄 모르는데 굳이 파이썬으로 서버 작업을 하고있음..)
잘 짜여진 레퍼런스 코드를 보고 이해하려고 해도 어느정도 감만 잡힐뿐.. 어떠한 논리적 관계도 알아차리기 힘들다.</p>
<p>오늘 7시간동안 삽질만 한듯..
그나마 내일은 적어도 GET/POST 기능이 구현될 수 있는 가능성이 살짝 보인다는 것에 기대를 가지고 오늘 작업을 마무리 해야겠다.</p>
<p><img src="https://images.velog.io/images/dev_2dong/post/c05dddb9-2b72-4a1f-a2ab-af510a1e7b34/6.JPG" alt="">
(뭔가 나름대로 많이 했는데 수확이 없다..)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android Clone App] 당근마켓 (1일차) 개발 환경 설정]]></title>
            <link>https://velog.io/@dev_2dong/Android-Clone-App-%EB%8B%B9%EA%B7%BC%EB%A7%88%EC%BC%93-1%EC%9D%BC%EC%B0%A8-%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@dev_2dong/Android-Clone-App-%EB%8B%B9%EA%B7%BC%EB%A7%88%EC%BC%93-1%EC%9D%BC%EC%B0%A8-%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Mon, 24 Aug 2020 13:43:40 GMT</pubDate>
            <description><![CDATA[<h1 id="000--서론">000 : 서론</h1>
<p>당근마켓 앱을 따라 만들어보기로 결심했다.
사실 4~5달 전 쯤에 무턱대고 도전했던 프로젝트였지만 실패했었다!
실력에 비해 수준이 높았던 탓으로... 인해 코드가 꼬이고, 데이터의 이동이 복잡해지고, 서버 사용에 대한 미숙함 (Firebase를 사용했었다) 때문에..?
거창하게 시작했던 프로젝트를 한 달뿐이 지나지 않았지만 손을 완전히 놓았던 것이었다...</p>
<p>이번에는 지난번보다 더 거창하게 프로젝트를 진행한다.</p>
<p>항상 프로젝트를 진행하면서 실패했던 이유를 짚어보면.. 가장 큰 이유는
<strong>설계의 부재</strong>가 아니었나 싶다.
항상 프로젝트 망하고나서.. 처음부터 내가 구조를 잘 짰다면 이런일이 없었을텐데 하고 말이다..
이번에는 철저한 준비를 통해 프로젝트를 성공적으로 끝낼 생각이다.
(급할 필요가 없다..)</p>
<p>이번에 사용하고 싶은 기술들은 아래와 같다.</p>
<ul>
<li>Clean Code</li>
<li>Kotlin</li>
<li>MVVM Pattern</li>
<li>Room + ViewModel + DataBinding</li>
<li>Coroutine</li>
<li>Koin</li>
<li>Material Component</li>
<li>Retrofit</li>
<li>Server Hosting</li>
</ul>
<p>어떤 프로젝트를 시작할 때, 새로운 기술의 도입은 최대 2개까지만 하라는 조언을 들은적이 있다.
이번에 도입하는 기술들은 Koin과 Coroutine이다.
아예 사용해보지 않은 것들은 아니지만 따로 학습을 진행하고 사용했던 것이 아니었다. 따라서, 직접 공부하고 구현해보면서, 단순히 코드 작성이 편리해지기 때문에 사용하는 목적을 깨우치기 위함이 아닌.. 원리와 지식까지 전체적으로 학습하며 사용 할 계획이다.</p>
<p>특히나 이번에 Server를 직접 구현하여 Hosting 해보려고 한다.
지난번 당근마켓 구현시 Firebase에 저장 되어있는 정보들을 control 하는 과정에서 애를 많이 먹었는데, 이러한 수고를 줄여줄 수 있는 방안이 될 수 있을 것이라고 생각했기 때문이다.</p>
<p>이 클론앱 시리즈는 앞으로 <code>일기</code>형식으로 작성 될 가능성이 매우 높다.
정보 전달의 목적이 아니라.. 내가 어떤식으로 작업했는지 그 흐름을 기록해 두고 싶을 뿐이기 때문이다.
(포스팅까지 신경써버리면 재미있게 시작했던 프로젝트가 일로 느껴질까봐.. ㅎㅎ)
그래서 원래 글 작성하면 맞춤법 검사도 철저하게 하고.. 띄어쓰기도 신경쓰는데 진짜 생각나는대로 적을 가능성 역시 매우 높다!</p>
<hr>
<h1 id="001--프로젝트-환경-조성">001 : 프로젝트 환경 조성</h1>
<p>서론에서 말했듯.. 이번 프로젝트는 코드 작업을 시작하기 전에 사전에 철저한 준비 작업이 선행 될 것이다.</p>
<h3 id="notion">Notion</h3>
<p>이를 고려하여 나의 작업 내용 관리를 진행 할 <code>notion</code> 페이지를 오픈했다.
<img src="https://images.velog.io/images/dev_2dong/post/f08e50be-61fb-480f-8f81-36c9089ad062/1.JPG" alt=""></p>
<p>노션은 개별적으로 진행중인 팀 프로젝트에서 사용을 해봤는데, 기본적으로 사용성이 괜찮은 사이트인것 같아서 채택했다. 기본적으로 작성 layout도 제공을 해주기 때문에, 손쉽게 나만의 노트를 만들 수 있었다.</p>
<h3 id="github">GitHub</h3>
<p>깃허브 저장소 역시 만들어놨다.
아직 아무것도 없으므로~ 때가 되면 링크를 달아두겠다.</p>
<h3 id="서버-개발-환경">서버 개발 환경</h3>
<p>사실 서버 개발은 처음인데... 정말 기초적인 서버 작업을 진행 할 생각이라, 서버는 만들어지기만 하면 돼! 하는 생각이 7할정도 머리속에 들어있는 상태로 작업 할 것 같다.</p>
<p>어떻게 구현하는 것인지 검색하는데 상당히 애를 먹었는데,
어쨌든 검색한 결과 <code>Python</code>의 <code>Django</code>를 이용하여 서버 구축을 진행 하기로 결정을 했다.
또한, Database는 <code>Mongo DB</code>를 사용하기로 했다.
Local 환경에서만 사용해도 괜찮을거라 생각했지만, 주로 집에서 데스크탑 작업을 하고.. 간혹 카페에서 노트북 작업을 진행하기 때문에 <code>Hosting Server</code>가 필요하다고 생각했다. 이게 이렇게 접근하는게 맞는지는 모르겠는데.. 같은 팀 프로젝트 백엔드 담당자 분께서 <code>Heroku</code>를 사용하여 서버 배포를 하셨던 것을 기억해서.. 나도 그것을 사용하기로 결정했다 ㅎㅎ </p>
<p>어쨌든 정리하면 서버 개발을 위해</p>
<ul>
<li>Python 및 IDE(Pycharm)</li>
<li>Django</li>
<li>Mongo DB</li>
<li>Heroku</li>
</ul>
<p>가 필요했으며, 이 중 Python 설치 및 Django Local Server까지는 오픈을 완료했다.
<img src="https://images.velog.io/images/dev_2dong/post/6765d319-0125-4e6f-b12d-b70ee59e1ca1/2.JPG" alt=""></p>
<p>이게 Local에서 열린 나의 서버인 것 같은데.. 정확히는 모르겠다 ㅎㅎ
어쨌든 성공했다고 하니깐 기분이 좋았다.</p>
<p>Heroku는 나중에 서버를 외부에 Open 할 때 사용할 것이므로 나중에 설치 예정이다.</p>
<hr>
<h1 id="003--간단한-기획-및-규칙-데이터구조-설정">003 : 간단한 기획 및 규칙, 데이터구조 설정</h1>
<p><img src="https://images.velog.io/images/dev_2dong/post/152ac7ed-8d2f-4644-babd-df44e4b8fc7f/4.JPG" alt="">
<img src="https://images.velog.io/images/dev_2dong/post/2c445fb6-0b6e-4aed-92fd-16e4c5063a6c/3.JPG" alt="">
<img src="https://images.velog.io/images/dev_2dong/post/02a4a323-48d6-4a59-adb9-cd8ad53fc1fa/5.JPG" alt=""></p>
<p>약간 이런식으로 notion에 기록을 해두었다.
첫 날이라서 다 한건 아닌데, 그래도 어플 보면서 분석해가면서 구조를 짜봤다.
이 작업은 이번주까지는 완료 할 생각이다.</p>
<hr>
<h1 id="생각">생각</h1>
<p>시작은 항상 재미있다.
나중에 구조가 엉망이 되지 않기를 소망하며..</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] 카카오맵 API 사용 설정 및 각종 오류 해결]]></title>
            <link>https://velog.io/@dev_2dong/Android-%EC%B9%B4%EC%B9%B4%EC%98%A4%EB%A7%B5-API-%EC%82%AC%EC%9A%A9-%EC%84%A4%EC%A0%95-%EB%B0%8F-%EA%B0%81%EC%A2%85-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@dev_2dong/Android-%EC%B9%B4%EC%B9%B4%EC%98%A4%EB%A7%B5-API-%EC%82%AC%EC%9A%A9-%EC%84%A4%EC%A0%95-%EB%B0%8F-%EA%B0%81%EC%A2%85-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Fri, 17 Jul 2020 14:05:12 GMT</pubDate>
            <description><![CDATA[<p>지도 API에 대한 학습을 진행하려고 구글맵, 카카오맵 중 어느것을 사용할까 고민을 했습니다.
구글맵의 유료 정책이 서서히 도입되는 것 같고,
어차피 국내에 한해 지도를 제작하는 현 상황만 고려한다면 카카오맵 학습을 하는것이 괜찮을 것 같아 사용을 시작했습니다.
그 과정에서 MapView를 띄우는 과정에서만도 알려진 오류란 오류는 다 걸려본 것 같아서.. ㅠㅠ
그래서 정리하는 겸 포스팅을 하기로 했습니다.</p>
<h1 id="카카오맵-api-사용-설정">카카오맵 API 사용 설정</h1>
<h3 id="개발자-등록-및-애플리케이션-추가하기">개발자 등록 및 애플리케이션 추가하기</h3>
<p><a href="https://developers.kakao.com/">https://developers.kakao.com/</a>
위 사이트에 접속하여 로그인을 해줍니다.
로그인 아이디는 자신이 사용하고 있는 <code>카카오톡 계정</code> 으로 로그인 하면 되기 때문에, 간단하게 접속 가능합니다. (카카오톡 안쓰시는 분들은 사실상 없을테니깐..?)</p>
<p>접속하면 개발자 등록이 가능합니다.
개발자 등록이 되어있지 않은 사용자일 경우 자동으로 설정창으로 이동되어 등록됩니다.</p>
<p>등록이 되어있는지 확인하기 위해서는
아래 사진 우측 상단의 빨간색 계정 정보를 클릭하신 뒤 <strong>계정 설정</strong> 탭을 누르셨을 때 이동하는 페이지에서 <strong>디벨로퍼스 프로필 정보</strong>란이 채워져 있다면 등록이 된겁니다.<br><img src="https://images.velog.io/images/dev_2dong/post/d1ee5788-8b15-447a-8e58-2dddfc98e149/200717_01.JPG" alt="">
<img src="https://images.velog.io/images/dev_2dong/post/50c0b553-d2d9-4c06-9129-1a0bc61d8484/200717_02.JPG" alt=""></p>
<p>이후, 애플리케이션을 추가해야합니다.
메인 홈 화면에서 <strong>내 애플리케이션</strong>을 클릭하신 뒤,
<strong>애플리케이션 추가하기</strong> 버튼을 눌러 작업을 진행합니다.
<img src="https://images.velog.io/images/dev_2dong/post/e77cc01e-58e9-4142-a1f2-d1567bbdb1d8/200717_06.JPG" alt=""></p>
<p><img src="https://images.velog.io/images/dev_2dong/post/3b7f88ad-1cd4-436e-8302-8670efd38437/200717_08.JPG" alt=""></p>
<p>연습용으로 사용할 경우 대강 정해주시면 됩니다.</p>
<p><img src="https://images.velog.io/images/dev_2dong/post/dee4bd46-3c5b-48af-90de-30c17ea381f6/200717_07.JPG" alt=""></p>
<h3 id="플랫폼-추가">플랫폼 추가</h3>
<p>만들어진 애플리케이션의 세부항목으로 들어가면 <code>요약 정보</code> 탭에 여러 앱키들이 존재합니다.
그리고 그 아래, 플랫폼을 설정하는란이 보입니다. (혹은 좌측 메뉴에 플랫폼 탭)
<img src="https://images.velog.io/images/dev_2dong/post/833caa68-a737-4ddf-be8c-c81f7bbcdff7/200717_09.JPG" alt=""></p>
<p>여기서 <strong>안드로이드 플랫폼 등록</strong>이 가능합니다.</p>
<p><img src="https://images.velog.io/images/dev_2dong/post/68951271-6a6e-45aa-b928-edaaf33ddb54/200717_10.JPG" alt=""></p>
<p><code>패키지명</code>은 사용하는 안드로이드 패키지의 명을 그대로 따 적어주면 됩니다.
<code>마켓 URL</code>은 패키지명 입력시 자동으로 부여되지만, 연습을 위한 프로젝트에서는 없음 설정하시면 됩니다.</p>
<p><code>키 해시</code> 값은 릴리즈 목적이 아닌 경우에는 디버그 키를 입력해주시면 됩니다.</p>
<pre><code class="language-kotlin">fun getAppKeyHash() {
        try {
            val info = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
            for(i in info.signatures) {
                val md: MessageDigest = MessageDigest.getInstance(&quot;SHA&quot;)
                md.update(i.toByteArray())

                val something = String(Base64.encode(md.digest(), 0)!!)
                Log.e(&quot;Debug key&quot;, something)
            }
        } catch(e: Exception) {
            Log.e(&quot;Not found&quot;, e.toString())
        }
    }</code></pre>
<p>위의 코드를 <code>MainActivity</code>의 <code>onCreate()</code> 등에서 실행시키면 로그에 key값이 찍힙니다.
이를 얻어와 입력해주시면 됩니다.</p>
<p>이후 저장하시면 해시 키 등록까지 완료됩니다.</p>
<h3 id="api-사용-설정하기">API 사용 설정하기</h3>
<p><a href="https://apis.map.kakao.com/android/guide/">https://apis.map.kakao.com/android/guide/</a>
위 사이트에 접속하시면 최신버전의 SDK를 다운받는 링크가 존재합니다.
글 작성기준 v.1.4.1.0 (2019/11/12) 버전이 최신입니다.</p>
<p>링크를 다운받고 압축을 푸시면 <code>libs</code>폴더안에 <code>libDaumMapAndroid</code>라는 jar파일 한 개와,
<code>arm***</code>형식의 폴더 3개(폴더 안에는 각각의 환경에서 적용되는 <code>libDaumMapEngineApi.so</code>가 들어있음)가 존재하는 것을 확인 가능합니다. </p>
<p>안드로이드 스튜디오에 들어갑니다. 
현재 파일 탐색이 android단에서 이루어지고 있습니다. 이를 아래 사진의 과정을 참고하여 <code>Project</code>로 바꿔줍니다.
<img src="https://images.velog.io/images/dev_2dong/post/3db9413d-45e3-4cca-bfc5-873a1b3971f2/200717_03.JPG" alt=""></p>
<p>이후, 다운로드 받았던 파일들을 하나씩 붙여넣기를 진행합니다.
우선, <code>libs</code>디렉터리에 <code>libDaumMapAndroid.jar</code>을 붙여넣어줍니다.
또한, <code>main</code>디렉터리에 <strong><code>jniLibs</code>라는 디렉터리를 생성</strong>해줍니다!
이후, <code>.so</code>파일이 들어있는 세 개의 폴더를 넣어줍니다.
최종적으로는 아래의 사진처럼 구성이 되어야합니다.
<img src="https://images.velog.io/images/dev_2dong/post/c5c30c83-83de-45c6-8f7e-1eb29f11cc9a/200717_04.JPG" alt="">
이후, 다시 android 탐색 단으로 돌아와줍니다.</p>
<h3 id="라이브러리-불러오기">라이브러리 불러오기</h3>
<p>[File] -&gt; [Project Structure]에서 <code>Dependencies</code>탭에 들어갑니다.
Jar Dependency를 추가하면 항목 선택란에
아까 붙여넣었던 <code>libDaumMapAndroid.jar</code> 항목 선택이 가능합니다.
등록해주시고 module단 gradle에 들어가시면 jar파일이 implement 되어있음을 확인할 수 있습니다.
<img src="https://images.velog.io/images/dev_2dong/post/63e89844-dcdd-4412-bd21-da3a63e86772/200717_11.JPG" alt="">
<img src="https://images.velog.io/images/dev_2dong/post/ae0c0aa6-feee-4aab-be87-ce7e82d36a89/200717_12.JPG" alt="">
<img src="https://images.velog.io/images/dev_2dong/post/229e34a4-3bd5-45c7-9c46-0be828cdea19/200717_13.JPG" alt=""></p>
<h3 id="manifests-설정-및-권한-설정">manifests 설정 및 권한 설정</h3>
<p>kakao developers 사이트의 애플리케이션 앱 키를 따옵니다.
android studio에서 사용할 <code>네이티브 앱 키</code>를 복사해둡니다.</p>
<p>manifests에 들어가서 application태그 안에 <code>meta-data</code>를 추가해줍니다.
이 때, activity 태그 안에 넣거나 application 태그 밖으로 넣지 않도록 주의합니다.</p>
<pre><code class="language-xml">&lt;meta-data android:name=&quot;com.kakao.sdk.AppKey&quot; android:value=&quot;your_app_key&quot;/&gt;</code></pre>
<p>또한, 지도의 여러 기능을 사용하기 위해 위치 권한 사용을 알립니다.</p>
<pre><code class="language-xml">&lt;uses-permission android:name=&quot;android.permission.INTERNET&quot;/&gt;
&lt;uses-permission android:name=&quot;android.permission.ACCESS_FINE_LOCATION&quot;/&gt;
&lt;uses-permission android:name=&quot;android.permission.ACCESS_COARSE_LOCATION&quot;/&gt;</code></pre>
<h3 id="mapview-사용">MapView 사용</h3>
<p>지도를 넣을 ViewGroup에 id 네이밍을 합니다.</p>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;
    tools:context=&quot;.MainActivity&quot;&gt;

    &lt;androidx.constraintlayout.widget.ConstraintLayout
        android:id=&quot;@+id/mapView&quot;
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;match_parent&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot;
        app:layout_constraintTop_toTopOf=&quot;parent&quot;
        app:layout_constraintBottom_toBottomOf=&quot;parent&quot;/&gt;

&lt;/androidx.constraintlayout.widget.ConstraintLayout&gt;</code></pre>
<p>저같은 경우는 자식 ConstriantLayout에 지도를 넣을것이고, <code>mapView</code> id를 부여했습니다.</p>
<p>이후 코드단에서 <code>MapView</code>객체를 만들어주고 ViewGroup의 <code>addView()</code> 메서드를 사용하여 지도를 띄워줍니다.</p>
<pre><code class="language-kotlin">val map = MapView(this)
mapView.addView(map)</code></pre>
<p><code>MapView</code>를 import할 때, <code>net.daum.mf.map.api</code>에서 불러오셔야 합니다.
이제 실행하시면 지도가 화면상에 보임을 확인할 수 있습니다.</p>
<p>오류에 대한 문제는 아래에 기술했습니다.</p>
<p>권한 처리는 <code>TedPermission</code>을 사용했고, 추가적인 언급을 하지 않았습니다.
이에 대해서는 각자의 권한 처리 방법에 따라 문제를 해결하시면 될 것 같습니다.</p>
<h1 id="만났던-오류들">만났던 오류들</h1>
<h3 id="cant-load-daummapengineapiso-file">Can`t load DaumMapEngineApi.so file</h3>
<p>진짜 삽질했던 오류입니다..
구글링해서 찾아보려고 하니 대부분 옛날 자료여서 예전에 있던 오류에 대한 해결책만 주구장창 나와서 애먹었습니다.</p>
<p>여기에서 오류가 날거라고 생각도 못했는데..
저 같은 경우의 해결책은 아래와 같았습니다.</p>
<p>MapView를 import할 때,
<code>net.daum.android.map</code>
<code>net.daum.mf.map.api</code>
중 어느 곳에서 데려올거냐는 이야기를 합니다.</p>
<p>여기서 아래꺼(net.daum.mf.map.api) 해주셔야 합니다... 위에거 하면 저처럼 튕깁니다.
<img src="https://images.velog.io/images/dev_2dong/post/9a4846cd-78af-494e-95c0-b456e7fcb52c/200717_05.JPG" alt=""></p>
<p>그리고, 추가적으로 위의 방법으로 문제가 해결 안되시는 분들은
jar파일과 so파일들이 디렉터리에 알맞게 위치해있는지 다시 한 번 확인하셔도 좋을 것 같습니다.</p>
<h3 id="kakao마크는-뜨지만-지도가-보이지-않는-현상">kakao마크는 뜨지만 지도가 보이지 않는 현상</h3>
<p>이제 앱은 튕기지 않는데, 지도의 이미지가 불러와지지 않는 현상이 발생했습니다.</p>
<pre><code>cleartext HTTP traffic to ot0.maps.daum-img.net not permitted</code></pre><p>위와 같은 에러 문구가 키워드인 것 처럼 뜨면서 말이죠.</p>
<p>이는, 기본적으로 지도 API가 http통신을 하면서 정보를 불러오는데, 안드로이드9 이상부터는 https 통신이 베이스이기 때문에 http통신에 대한 예외 처리를 해주지 않으면 정보를 얻어올 수 없어 발생하는 문제입니다.</p>
<p>해결책은 아래와 같습니다.</p>
<ol>
<li><p>resource에 xml파일 추가 (network_security_config.xml)</p>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;network-security-config&gt;
 &lt;domain-config cleartextTrafficPermitted=&quot;true&quot;&gt;
     &lt;domain includeSubdomains=&quot;true&quot;&gt;maps.daum-img.net&lt;/domain&gt;
 &lt;/domain-config&gt;
&lt;/network-security-config&gt;</code></pre>
</li>
<li><p>manifests에 다음 속성 추가</p>
<pre><code class="language-xml">android:usesCleartextTraffic=&quot;true&quot;
android:networkSecurityConfig=&quot;@xml/network_security_config&quot;</code></pre>
<p>이후 실행하시면 지도가 정상적으로 화면에 나타나는 것을 확인할 수 있습니다.</p>
</li>
</ol>
]]></description>
        </item>
    </channel>
</rss>