<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>hong_journey.log</title>
        <link>https://velog.io/</link>
        <description>DEEP DIVER</description>
        <lastBuildDate>Sun, 30 Mar 2025 07:11:16 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>hong_journey.log</title>
            <url>https://images.velog.io/images/hong_journey/profile/940092f1-03bc-43df-9e93-141aea7f0887/블로그 프로필 사진.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. hong_journey.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/hong_journey" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[안 하느니만 못한 00]]></title>
            <link>https://velog.io/@hong_journey/%EC%95%88-%ED%95%98%EB%8A%90%EB%8B%88%EB%A7%8C-%EB%AA%BB%ED%95%9C-00</link>
            <guid>https://velog.io/@hong_journey/%EC%95%88-%ED%95%98%EB%8A%90%EB%8B%88%EB%A7%8C-%EB%AA%BB%ED%95%9C-00</guid>
            <pubDate>Sun, 30 Mar 2025 07:11:16 GMT</pubDate>
            <description><![CDATA[<p>조금 과격한 제목일 수도 있지만 말 그대로 안 하느니만 못한 일은 하지 말자고 다짐했다. 분석 업무를 할 때도, 미팅을 준비할 때도, 로그 QA를 할 때도 같은 마음으로 임했다. 하지만 조금은 정신없는 평일이 지나고 주말이 되어 내가 진행한 업무 기록을 다시 읽거나 기억 속에서 되짚어보면 이런 생각들이 들었다. </p>
<p>&#39;이건 어찌 보면 당연한 말이잖아.&#39;
&#39;그래서 결국 결론이 뭐지&#39; </p>
<p>꼼꼼하게 기록한 로그 문서도, 긴 고민 끝에 분석하고 정리한 결과도 시간을 두고 다시 읽어보면 그렇게 시간과 노력을 오래 들인 만큼 의미 있는 내용이었나, 확신이 들지 않았다. 그렇다고 잘 아는 척하며 어물쩍 넘어가는 사람이 되고 싶지도 않았다. &#39;이건 내가 해봐서 아는데~ 이건 당연히 ~ 예요&#39; 같은 말을 하는 나 자신을 도저히 용납할 수 없었다. 그건 정말이지 내가 가장 되고 싶지 않은 종류의 모습이었다.  </p>
<p>계절이 바뀌고 오랫동안 방치된 방구석 청소를 시작하듯, 글또를 시작하면서 비로소 케케묵은 고민들을 펼쳐놓고 정리란 것을 시작했다. </p>
</br>

<h1 id="글또-10기에-작성한-글">글또 10기에 작성한 글</h1>
<ol>
<li><p><a href="https://velog.io/@hong_journey/%EA%B8%80%EB%98%90-10%EA%B8%B0%EB%A5%BC-%EC%8B%9C%EC%9E%91%ED%95%98%EB%A9%B0">글또 10기를 시작하며</a></p>
</li>
<li><p><a href="https://velog.io/@hong_journey/%EB%8C%80%EC%8B%9C%EB%B3%B4%EB%93%9C%EB%8A%94-%EC%96%B8%EC%A0%9C-%ED%95%84%EC%9A%94%ED%95%A0%EA%B9%8C">대시보드는 언제 필요할까</a></p>
</li>
<li><p><a href="https://velog.io/@hong_journey/%EC%8B%A4%EC%88%98%EC%9D%98-%EC%9E%AC%EA%B5%AC%EC%84%B1">실수의 재구성</a></p>
</li>
<li><p><a href="https://velog.io/@hong_journey/%EB%AA%A8%EB%93%88-%EB%A1%9C%EA%B9%85%ED%95%98%EA%B8%B0">모듈 로깅하기</a></p>
</li>
<li><p><a href="https://velog.io/@hong_journey/%ED%83%9C%EB%B8%94%EB%A1%9C-LOOKUP-%ED%95%A8%EC%88%98%EC%9D%98-%EC%93%B8%EB%AA%A8">태블로, LOOKUP 함수의 쓸모</a></p>
</li>
</ol>
<p>지금 글을 포함하면 총 12회 중 6개.. 글을 작성했다. 
일주일 전에 무슨 일이 일어났는지도 솔직히 가물가물한데, 한동안 머릿속에 둥둥 떠다녔던 고민이 휘발되지 않고 기록으로 남게 되어 다행이다. 
한번 기록으로 정리하고 나니까 누군가에게 고민을 이야기하며 조언을 구할 때 조금 더 정돈되고 체계적인 방식으로(?) 질문할 수 있다는 장점도 있었다. </p>
<p>그렇지만 개인적으로 아쉬운 점도 많다. 
우선 커피챗이나 오프라인 행사에 적극적으로 참여하지 못한 것이 아쉽다. (이번에 글또 슬랙에 자주 안 들어가는 바람에 오프라인 행사를 대부분 놓쳤다) 전체 회차 중 절반만 제출했기 때문에 지속성 측면에서 아쉽고, 카테고리를 골고루 작성하지 못했으며, 특히나 공부하고 새로 배운 내용을 정리하는 글은 거의 작성하지 못했다. 최근 6개월 동안 내가 그다지 성장했다는 느낌이 들지 않아서 후회된다. </p>
</br>

<h1 id="앞으로">앞으로</h1>
<p>그럼 글또를 마무리하며 <strong>나는 어떤 방향의 성장을 추구하나?</strong> 를 고민해 보았다. 만약 이 질문에 대한 답을 할 수 없다면, 나는 그저 &#39;저 사람보다 잘해야지&#39; 하고 남과 나를 끊임없이 비교하는 방식으로만 나아가는 인생을 보낼 것 같았기 때문이다. </p>
<p>나는 <code>하나의 정답을 찾기보단, 여러 방법의 장단점을 알아가고 상황에 따라 다른 선택할 줄 아는 사람</code>이 되고 싶다. 조금 추상적인 목표일 수 있지만 블로그에 쓴 글로 예를 들면, 나는 시간이 지나 내가 쓴 글의 내용을 엎는 또 다른 글을 작성해 보고 싶다. 글또에서 작성한 글 역시 내가 한동안 깊게 고민하며 정리한 결론들이지만, 시간이 지나 경험의 폭이 넓어지고 지금과 또 다른 견해를 가지게 될 수도 있다. </p>
<p>나는 내가 썼던 글을 다시 읽었을 때 &#39;그래 이거야&#39;하고 만족하는 순간보다, 나의 주장에서 구린 점을 발견하고 새로운 견해로 업데이트하는 과정에서 큰 재미를 느낀다는 것을 알게 되었다. 과거 주장을 번복하려면 일단 당시 생각을 정리해 놓은 무언가가 있어야 하는데 만약 글또가 없었더라면 이런 글을 작성하지 못했을 것이다. 그래서 글또에 신청하길 정말 잘한 것 같고, 출석률이 높지 않지만 그래도 완주해서 다행이라는 생각이 든다. </p>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/9f3ef0ae-6cc8-4321-85ba-f1877a0c3f73/image.jpeg" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[태블로, LOOKUP 함수의 쓸모]]></title>
            <link>https://velog.io/@hong_journey/%ED%83%9C%EB%B8%94%EB%A1%9C-LOOKUP-%ED%95%A8%EC%88%98%EC%9D%98-%EC%93%B8%EB%AA%A8</link>
            <guid>https://velog.io/@hong_journey/%ED%83%9C%EB%B8%94%EB%A1%9C-LOOKUP-%ED%95%A8%EC%88%98%EC%9D%98-%EC%93%B8%EB%AA%A8</guid>
            <pubDate>Sun, 16 Mar 2025 14:32:57 GMT</pubDate>
            <description><![CDATA[<h1 id="1-수치를-이해할-때-도움이-되는-지표">1. 수치를 이해할 때 도움이 되는 지표</h1>
<p>대시보드에 숫자가 굉장히 많을 때, 이런 설명이 함께 있으면 이해에 도움이 되는 것 같다. </p>
<ul>
<li>전월 동기간 대비 n% 증가</li>
<li>이번달 목표 대비 m% 달성</li>
<li>전체 가입자수 대비 n% 달성</li>
</ul>
<p>이중 &quot;전월 동기간 대비 n%&quot; 를 표시하고 싶을 때 가장 단순한 방법은 태블로로 추출할 마트 테이블에 컬럼을 추가하는 것이다. 예를 들면 LAG 함수를 써서 지난달에 해당하는 값을 row마다 가져오는 방식이다.</p>
<p>하지만 한창 대시보드를 만들고 있다가 뒤늦게 지표를 추가하려고 하면, 마트를 다시 만들어야하는 과정이 매우 번거롭다. 이때 태블로에서 사용 가능한 LOOKUP 함수를 알게되어 사용법을 정리해보았다. </p>
<p>데이터는 태블로 클라우드의 기본 데이터 Superstore Datasource를 이용했다.</p>
</br>


<p><img src="https://velog.velcdn.com/images/hong_journey/post/f7093e02-55e3-4017-9aad-624e8ab96d20/image.png" alt=""></p>
<h1 id="2-필요한-것은">2. 필요한 것은?</h1>
<ul>
<li>기준일자를 조정하면서</li>
<li>지난주 같은 요일 대비 증감을 확인하고 싶다. </li>
<li>(디자인도 개선이 필요하지만 일단 이건 패스..)</li>
</ul>
</br>

<h2 id="21-매개변수-만들기">2.1 매개변수 만들기</h2>
<p>최신 데이터만 보여주지 않고, 기준일자를 조정하는 리모컨을 만들고 싶다면 매개변수를 만들면 된다. </p>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/93a45a59-ff25-4d15-9d43-761e2b04f02b/image.png" alt=""></p>
<p>좌측 상단 메뉴에서 매개변수 만들기를 누르고, 데이터 유형 및 기본값을 정의한다. 태블로 데스크탑 유료 제품에선 통합 문서가 열릴 때의 값을 max_date (추출된 데이터중 가장 최근값)으로 지정 가능한 것 같은데 여긴 클라우드라서 안 되는 것인지? 아니면 데이터 자체가 추출방식으로 가져오는 것이 아니라서 그런 것인지 잘 모르겠다. </p>
<p>그리고 매개변수 이하 날짜들만 보여줄 것이므로 새로 boolean 변수를 생성한다. </p>
<ul>
<li>order date가 기준일자 이하면 참, 초과하면 거짓</li>
</ul>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/98e45048-881d-4983-8d23-6cd57f826f28/image.png" alt=""></p>
<p>그리고 필터에 추가하여 &quot;참&quot;일 때만 보여준다.
&quot;매개변수 표시&quot;한 다음, 매개변수(target date)의 날짜를 조정하면 해당 날짜까지의 데이터만 노출된다.</p>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/99e1209c-352c-4f86-ad04-09253d74a0e3/image.png" alt=""></p>
</br>

<h2 id="22-지난주-대비-변화">2.2 지난주 대비 변화</h2>
<p>기준일자 or 기준일자 - 7days 일자만 필터에서 변화를 표현하고 싶다. 이것도 boolean으로 정의한다. </p>
<p>함수 사용법이 헷갈릴 땐 오른쪽 함수 검색을 사용한다.</p>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/b6a04e25-55fe-476f-a666-e2c10f86655d/image.png" alt=""></p>
<p>방금 정의한 boolean 변수와 Order Date를 같이 행에 올린 다음, 측정값(Quantity)는 셀에 올린다. </p>
<p>{기준일자}와 {기준일자 일주일 전} 데이터만 나타난 것을 확인할 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/7687afcd-106e-4e64-83f2-4057266f6550/image.png" alt=""></p>
<p>이제 변화한 양을 계산할 것인데</p>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/67917d5b-4406-47bb-8674-a01190c3cca6/image.png" alt=""></p>
<ul>
<li><code>LOOKUP</code> : 한 단계 다음 값을 가져옴. 쿼리에서 LEAD 함수와 유사하다. (그림처럼 offset이 음수라면? LAG 함수처럼 이전 단계 값을 가져온다.) </li>
<li><code>ZN</code> : if x is null then 0 else x (NULL 대신 0을 반환하는 함수)</li>
</ul>
<p>여기서 주의할 점은 LOOKUP 함수는 입력값이 반드시 집계치여야한다는 것이다. (x 가 아니고 SUM(x) 여야함)</p>
<p>이 데이터의 경우 date 일자별로 quantity 값이 중복은 아니기에 SUM 함수를 씌워도 무방하다. (SUM을 씌울 때는 항상 row 가 중복이 아닌지 체크)</p>
<h2 id="23-레이블-편집">2.3 레이블 편집</h2>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/32f1e5d5-c9c8-4bfc-a97e-ab8134c0e9bf/image.png" alt=""></p>
<p>만약 증가했다면 빨간색으로, 감소했다면 파란색으로 색상을 표시하고 싶다면 변수를 추가로 정의한다. </p>
<ul>
<li>amount_change_plus : <code>if amount change &gt;= 0 then amount change end</code></li>
<li>amount_change_minus : <code>if amount change &lt; 0 then amount change end</code></li>
</ul>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/b59535ef-349e-4cec-b48c-9136a03aa60f/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/322d7bf1-500a-4819-9f78-91f9565f7eaf/image.png" alt=""></p>
<p>그리고 이렇게 레이블 편집을 하면, 양수라면 빨간색으로 음수라면 파란색으로 표시가 될 것이다</p>
<p>.. 라고 적어놓고 보니 다른 대안은 정말 없는 것일까..? ㅎㅎ</p>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/20f12112-3c4d-45f8-80c3-78251f275014/image.png" alt=""></p>
<p>사용자 서식으로 기호도 추가 가능하다. 
자세히 알아보기를 눌러서 태블로 문서를 읽어보니, 구문은 세미콜론으로 구분하며 차례대로 양수, 음수, 0 이렇게 세 부분으로 구성된다고 한다.</p>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/8ca1d115-6286-4ff1-8661-820d20cfe5c2/image.png" alt=""></p>
<p>그리고 이제 기준일자만 표시하기 위해 지난주에 해당하는 날짜는 &quot;숨기기&quot; 처리를 해준다.</p>
<p>참고로 &#39;제외&#39;, &#39;이 항목만 유지&#39; 는 필터 조정 기능이다. 그래서 &#39;제외&#39;를 사용하면 지난주 수치가 필터로 제외되기 때문에 LOOKUP 함수가 제대로 작동하지 않는다. </p>
</br>

<p>그런데.. 여기까지 진행한 다음, 매개변수로 날짜를 또 다른 값으로 조정하면 지난주 값이 숨겨지지 않고 다시 화면에 나타나는 (?) 현상을 겪게 될 것이다.</p>
<p>방금 &quot;지난주 날짜&quot;를 숨기지 않고 특정 일자 셀(2024.08.26)을 숨겼기 때문이다. </p>
<p>특정 날짜를 숨기는 대신 &quot;지난주&quot; 값을 숨기려면, boolean 값으로 target_date 인지 여부를 새로 정의한다.</p>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/839bc058-f7cd-4313-843a-d1a0ebc2428a/image.png" alt=""></p>
<p>그 다음 boolean 값이 target_date 가 아닐 때 &quot;숨기기&quot;를 적용한다.</p>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/be9352f8-0c68-4fbd-86da-d49a80ca547e/image.png" alt=""></p>
<p>이제 매개변수를 조정할 때마다 
기준일자의 Quantity와 지난주 같은 요일 대비 증감이 변화하는 것을 확인할 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/e7392896-0fce-4735-92ba-8eeab0d62352/image.png" alt=""></p>
<h1 id="3-마무리하며">3. 마무리하며</h1>
<p>시각화 관점에서 파이썬과 태블로를 난이도로 비교하면, 나에겐 태블로가 더 어렵고 구글링 자체도 쉽지 않은 것 같다. 태블로의 기본 용어가 익숙하지 않은 상태이다보니, 구글 검색보다는 chat gpt에게 물어보는 것이 좀 더 의도한 답변을 얻는 경우가 많았다. </p>
<p>오히려 누군가가 만든 대시보드를 로컬에 다운 받아 변수 생성과 세팅을 그대로 따라하는 것도 괜찮은 방법 같다. LOOKUP 함수도 누군가의 태블로의 대시보드를 뜯어보다가 알게 되었다. 깨달음을 주신 귀인들에게 감사함을 느끼며 이 글을 바친다..</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[모듈 로깅하기]]></title>
            <link>https://velog.io/@hong_journey/%EB%AA%A8%EB%93%88-%EB%A1%9C%EA%B9%85%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hong_journey/%EB%AA%A8%EB%93%88-%EB%A1%9C%EA%B9%85%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 16 Feb 2025 14:58:16 GMT</pubDate>
            <description><![CDATA[<p>지난 1년간 플랫폼 팀에서 일하며 모듈 제품 로깅하는 업무를 맡았다. </p>
<p>모듈이란? 여러 종류의 서비스를 만들다보면 제품마다 공통적으로 필요한 기능들이 있다. </p>
<p>이런 기능의 동작을 일관된 경험으로 제공하도록 모듈을 만든다면? 제품팀에서는 플랫폼팀에서 이미 만들어놓은 모듈을 적용함으로써 적은 비용으로 안정적인 제품을 만들 수 있다. 유저 입장에서는 여러 서비스를 사용하더라도 동일한 모듈을 만나게 될테니 일관된 경험을 할 수 있다. </p>
<p>이렇듯 모듈은 장점이 많으니 무엇이든지 모듈화면 좋을 것 같지만, 플랫폼 팀 입장에서는 여러 고민이 든다. 안정적이고 일관된 제품을 만들기 위해서 정책은 어떻게 단순화하면 좋을까? 어디까지 커스텀을 허용할까? (A팀의 요구사항이 앞으로 다른 팀에서도 필요할까?) 등등.. </p>
<p>앱로그를 정의하는 입장에서도 어디까지 로그를 남길 것인지, 이후 로그 관리는 어떻게 해야할지 고민이 되었다. 오늘은 모듈 데이터를 분석하는 상황을 가정하고 어떤 점을 고민하며 모듈 앱로그를 정의하고 관리했는지 정리했다.</p>
<h1 id="식별-key-값-정의하기">식별 key 값 정의하기</h1>
<p>모듈은 여러 제품에서 사용한다. 그래서 특정 서비스 A의 모듈 데이터를 보고 싶다면, 모듈이 사용된 위치를 식별하기 위한 id 값을 꼭 남겨야한다. </p>
<p>우선 서비스와 모듈 관계가 1:1 인지 확인한다. 만약 서비스마다 모듈이 최대 1개만 노출된다면 service_id (서비스 식별값)을 key 로 남길 수 있을 것이다. </p>
<p>하지만 하나의 서비스에서 여러 위치에서 모듈을 노출시키는 경우도 많다. 이런 경우 모듈을 식별할 값을 새로 정의한다. 서버 개발자와 논의하여 제품에서 사용하는 key 값을 앱로그에도 동일하게 사용하면 좋다. 되도록이면 서버 개발자가 생성한 디멘젼 테이블의 값을 그대로 사용하면 가장 좋다. 앱로그와 테이블을 동일한 key 값으로 바로 join 해서 볼 수 있어 활용하기 좋고 추후 값을 관리하기도 좋기 때문이다.</p>
<h1 id="플랫폼마다-로그가-동일한-방식으로-남는가">플랫폼마다 로그가 동일한 방식으로 남는가</h1>
<p>모듈에서 제공하는 플랫폼은 크게 3가지가 있었다. 안드로이드 화면, iOS 화면, Web 화면. </p>
<p>앱로그를 사용하는 사람이 누굴까? 제품팀 사람들이다. 그들은 제품을 출시후 데이터 빠르게 확인해서 제품 개선 지점을 파악하려고 한다. 그리고 모듈은 그 퍼널중 일부분이다. 그런데 모듈 앱로그가 플랫폼마다 다르게 남는다면 데이터를 조회하고 분석하는 과정에 비효율을 초래할 것이다. 그래서 &#39;플랫폼마다 동일한 방식으로 로그가 남는가&#39;를 중요하게 생각하고 모든 플랫폼마다 로그 QA를 진행했다. </p>
<p>물론 100% 동일하게 남지는 않는다. 예를 들어 이탈 동작 로그가 다를 수 있다. 안드로이드는 우측 하단에 백버튼이 있고, iOS는 백스와이프로 이탈할 수 있는 등 이탈 방식 자체가 달라서 어쩔 수 없는 부분도 있다. 하지만 퍼널 전환율에 사용되는 로그만큼은 동일한 방식으로 남도록 관리했다. </p>
<h1 id="로그-이슈-재발-방지를-위해-모니터링">로그 이슈 재발 방지를 위해 모니터링</h1>
<p>모듈은 로그 QA를 완료했더라도 코드 리팩토링 과정이나 특정 서비스 환경에서 언제든지 이슈가 발생 가능하다. </p>
<p>모듈 제품을 출시 후 여러 팀으로부터 로그 이슈를 제보받았다. 초기에는 이슈 로그를 대체해서 볼 수 있는 방법을 제공하는 등 일회성 대응에 머물렀다. 하지만 시간이 지나고 보니 반복되는 이슈들이 몇 가지 있었다. 이런 중요 로그 이슈를 감지하는 슬랙봇 얼럿을 만들어 모니터링을 구축하기 시작했다. 이로인해 적어도 동일한 이슈가 재발할 때 바로 개발자분들과 확인하여 문제를 신속하게 해결할 수 있게 되었다. </p>
<h1 id="마무리하며">마무리하며</h1>
<p>모듈 로그가 없던 시절과 대비해서 지금 가능해진 것은?</p>
<ul>
<li>사용자가 모듈에서 어떤 문구를 보고, 그중 어떤 버튼을 클릭했는지, 어떤 방식으로 이탈했는지 등등 퍼널 전환율을 볼 때 중요한 로그들은 일관된 기준으로 자동으로 남게 되었다.</li>
<li>특정 위치에 뜨는 모듈의 지표를 확인하고 싶을 때 표준화된 key 값으로 데이터를 조회 가능해졌다. </li>
<li>서비스가 달라도, 플랫폼이 달라도(Android, iOS, web) 로그가 동일한 방식으로 남게되었다. </li>
<li>개별 제품팀에 속한 데이터 직군의 로그 QA시간이 줄어들었다. </li>
</ul>
<p>다음 글에서는 모듈 앱로그를 마트화하고 대시보드를 만들어 제품팀에 공유하는 과정에서 들었던 고민을 정리해보겠다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[실수의 재구성]]></title>
            <link>https://velog.io/@hong_journey/%EC%8B%A4%EC%88%98%EC%9D%98-%EC%9E%AC%EA%B5%AC%EC%84%B1</link>
            <guid>https://velog.io/@hong_journey/%EC%8B%A4%EC%88%98%EC%9D%98-%EC%9E%AC%EA%B5%AC%EC%84%B1</guid>
            <pubDate>Sun, 19 Jan 2025 14:54:47 GMT</pubDate>
            <description><![CDATA[<p>최근에 실험 결과를 집계하다가 큰 실수를 했다. 기존안 대비 실험안의 지표가 개선되었다고 결론을 내렸는데, 알고 보니 집계 과정에 오류가 있었고 실험안이 지고 있었던 것이다. 더군다나 (위너라고 판단한) 실험안을 기반으로 다음 실험을 준비하고 있었는데, 오류를 인지한 시점은 실험 결과를 공유한 후 2주가량 지난 시점이었다. 결국 나의 실수로 인해 개발자와 디자이너, PO 등 팀원들의 시간과 리소스가 2주가량 낭비되는 결과가 초래되었다. 데이터로 잘못된 의사결정을 내렸을 때 팀에게 미치는 영향이 얼마나 큰지 비로소 실감했다.</p>
<h1 id="사건-요약">사건 요약</h1>
<ul>
<li>퍼널 전환율 개선을 위해 실험을 오픈했고, 나는 로그 정의와 실험 집계를 맡았다.</li>
<li>사용자가 시도를 할 때마다 발번되는 session id 값이 있었다. 이것을 기준으로 처음과 끝을 left join하여 전환율을 구했다.</li>
<li>그런데 알고 보니.. 중간 단계에서 session id값이 재발급되는 로직이 있었다. 한번 생성된 값이 그대로 유지된다고 인지하고 있었기 때문에 실험 결과 지표가 과소집계되고 있었다.</li>
<li>실험 종료를 앞둔 시점에 분석 결과를 개발자와 DA에게 공유하며 같이 결과를 해석하는 과정에서 이 사실을 발견했다. </li>
</ul>
</br>

<h1 id="무엇을-놓쳤나">무엇을 놓쳤나</h1>
<p>쿼리 검증을 여러번 했다면 이 실수를 좀더 빨리 발견할 수 있었을까? 그럴 확률은 매우 희박해보였다. 다시 이런 실수를 반복하지 않기 위해 무엇을 바로잡아야할까 고민하던중.. 마침 읽고 있던 책인 &lt;유시만의 글쓰기 특강&gt;에서 내 생황에 일대일로 대응되는(?) 부분을 발견했다. </p>
<p>아래는 &quot;주장은 반드시 논증하라&quot; 챕터에서 발췌하고 내가 겪은 상황을 적용해본 글이다.</p>
<blockquote>
<p>*<em>가설은 반드시 논증되어야한다. *</em></p>
</blockquote>
<ul>
<li>논증하려면 근거나 이유를 밝혀야한다. 먼저 기준이 되는 <strong>지표</strong>를 제시한다. 정한 지표 기반으로 데이터를 요약함으로써 가설을 증명한다.<blockquote>
<ul>
<li>이럴 경우 다른 사람은 그 가설에 동의할 수도 있고 반박할 수도 있다. 가장 손쉬운 방법은 지표에 이의를 제기하고 다른 지표를 제안하는 것이다. 지표는 받아들이면서 다른 근거로 반박할 수도 있다. 예컨대 기존에 A라는 전환율이 70% 라는 것을 알고 있는데, 실험 결과를 계산해보니 A의 전환율이 50%였다면, 집계 과정의 오류를 의심하거나 표본이 대표성을 띄지 못한다고 이의를 제기할 수 있는 것이다. </li>
</ul>
</blockquote>
</li>
</ul>
<p>이렇듯 주장을 하기 위해서는 가설과 논증의 과정을 반복하는 과정이 꼭 필요하다. 이 과정을 혼자 진행하면 객관성이 떨어질 수 있고 분명 한계가 있기 때문에, 다른 사람에게 검증을 요청하는 것도 좋은 방법이다. 처음에 미리 구상해놓은 큰그림에 데이터를 이리저리 끼워맞추는 일만큼은 절대 하면 안 된다. 논증하지 않은 가설은 일종의 취향에 불과하다.</p>
</br>

<h1 id="그래서-실험-오픈을-위해-해야할-것">그래서 실험 오픈을 위해 해야할 것</h1>
<p><strong>실험 오픈전 해야할 일</strong></p>
<ul>
<li>1) 로그 정의, 로그 QA하기</li>
<li>2) 어떤 지표로 위너 선정할지 핵심 지표 정하기</li>
<li>3) 실험과 상관 없이 변동이 생기면 안 되는 가드레일 지표 정하기 </li>
<li>4) 핵심 지표와 가드레일 지표가 기존에는 어느정도 수준인지 확인하기 </li>
</ul>
<p>이번에 놓쳤던 부분이 4번이다. 이번에는 실험에 편입된 기존안과 실험안의 전환율만 비교했다. 평소에 이 전환율이 어느 정도라는 것을 실험 오픈 전에 알고 있었다면, 실험 결과에서 기존안의 전환율이 유독 낮은 이유를 이상하다고 생각했을 것이고 좀 더 빠르게 집계 오류를 찾을 수 있었을 것이다. </p>
<p>평소에 이 지표를 대시보드로 모니터링하고 있었다면 바로 확인 가능할 것이고, 그렇지 않다면 실험 오픈 전에 기존 지표가 어느정도인지 미리 집계해서 확인해야한다. 실험 기간을 산정하기 위해 대략적으로 계산하는 것이 아니라, 세부 조건까지 확인해서 정확하게 확인해야한다. (e.g. OS간 차이가 있지는 않은지, 실험 대상 한정하기 위해 분모의 조건을 더 세밀하게 정의할 필요는 없는지)</p>
</br>

<p><strong>실험 오픈 후 1~2일 후 해야할 일</strong></p>
<ul>
<li>1) 기존안과 실험안 로그가 기대했던 대로 남는가 (e.g. 실험안 로그가 기존안에도 남고 있지는 않은가)</li>
<li>2) 실험 오픈전에 집계했던 수치와, 실험 오픈 후 기존안의 수치가 어느정도 비슷한가</li>
<li>3) 실험 세팅이 잘 되어 있는가 (e.g. 실험 타겟 설정)</li>
<li>4) 집계 쿼리 로직이 정확한가</li>
</ul>
<p>당연한 것 아냐? 라고 생각했던 투두리스트지만, 뼈아프게도 내가 한번씩 실수해본 경험이 있는 항목들이다.. 일련의 크고 작은 경험들로 인해 나는 더이상 나를 믿지 않기로 했다..! 적어도 지금까지 한 실수는 다시 반복하지 않도록 당분간 이 점검 리스트를 실험 오픈할 때마다 캘박해놓고 습관으로 만들어야겠다. </p>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/96c7084b-9988-47ef-ab25-7685cc16188d/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[대시보드는 언제 필요할까]]></title>
            <link>https://velog.io/@hong_journey/%EB%8C%80%EC%8B%9C%EB%B3%B4%EB%93%9C%EB%8A%94-%EC%96%B8%EC%A0%9C-%ED%95%84%EC%9A%94%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@hong_journey/%EB%8C%80%EC%8B%9C%EB%B3%B4%EB%93%9C%EB%8A%94-%EC%96%B8%EC%A0%9C-%ED%95%84%EC%9A%94%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Sun, 24 Nov 2024 09:24:40 GMT</pubDate>
            <description><![CDATA[<h1 id="대시보드가-필요한-순간">대시보드가 필요한 순간</h1>
<p> 대시보드는 날마다 숫자가 자동으로 업데이트되는 편리한 도구다. 배포 이후 사용자가 제품을 잘 사용하고 있는지 보기 위해, 우리 팀이 목표한 지표를 현재 어느 정도 달성했는지 확인하고 싶어서, 그리고 주기적으로 추출하고 확인해야할 대상이 있을 때 등등.. 다양한 배경으로 대시보드를 요청받는다.</p>
<p> 그러나 시간이 지나, &#39;이 대시보드가 필요했었나?&#39;라고 질문해 보면 모든 순간이 꼭 그렇지는 않았다. 만들어놓고 나만 보는 대시보드도 있고, 관리를 해주지 않아 신뢰를 잃어버리고 잊히는 대시보드도 있다.</p>
<p> 그런데 대시보드 하나 만들기 위해서는 수많은 시간이 들어간다. 앱로그가 잘 들어오는지 로그 qa를 하고, 예상과 다르게 들어오는 데이터를 발견하면 이유를 찾기 위해 하나하나 까보기도 하고, 전반적으로 데이터 정합성이 의심되는 경우 앱로그와 서버테이블을 대조하여 검증하기도 하며, 대시보드를 그리다가 테이블 구조의 변경이 필요해서 마트 한판을 다시 만들어야 할 때도 있다. 그리고 대부분 이러한 작업이 or 조건이 아니라 and 조건으로 필요하다.</p>
<p><strong>대시보드는 제작 목적이 명확해야한다</strong>, 가 머리로는 이해되지만 여전히 새로운 대시보드 요청을 받는 순간 고민이 든다. &#39;주기적으로 모니터링할 만큼 중요한 지표일까?&#39;를 판단하기 위한 근거가 마땅히 떠오르지 않기 때문이다. 그래서 요즘은 일단 요청을 받은 대로 대시보드를 만들어보고, 어떤 지표가 문제 상황마다 잘 사용되는지 혹은 시간이 지나 아무도 궁금해하지 않는지 메모해 보기로 했다.</p>
</br>
</br>

<h1 id="대시보드도-제품인가">대시보드도 제품인가?</h1>
<p> 내 생각에는 SSAP 그렇다. 이 제품의 사용자는 내 동료들이고, 대시보드로 시각화하고자 하는 문제 상황에 대해 나보다 더 깊게 고민해 봤을 확률이 높다. 최근에 팀원들로부터 받은 피드백을 토대로 대시보드를 업데이트할 기회가 있었는데, 이때 질문받은 내용과 몇 가지 개선했던 것들을 정리했다.</p>
</br>

<h2 id="1-여기서-무엇을-봐야하나요">1. 여기서 무엇을 봐야하나요</h2>
<p> 처음에는 사람들이 대시보드를 어떻게 사용할지 상상했다. 앞으로 제품을 여러 차례 개선하게 될 텐데, 액션 전후로 무엇이 달라졌는지 다각도로 살펴볼 수 있어야 하지 않을까? 그래서 &#39;뭘 좋아할지 몰라 다 준비&#39;했다. 주요 클릭율과 전환율을 리스트업하고 대시보드에 꽉 차게 배치했다. 하지만 팀원들에게 초안을 공유했을 때 돌아오는 반응은 예상과 달랐다. &#39;무엇을 봐야 하는지 헷갈린다&#39;는 의견이 대다수였다. </p>
<p> 여러 지표를 그려야 했기 때문에 초안을 만들기까지 이미 시간도 오래 걸린 상황이었다. 피드백을 듣고 다시 대시보드를 들여다보니 팀원의 말에 공감이 갔다. 무엇이 가장 중요한지 눈에 들어오지 않았고, 지표가 많아서 어느 순서대로 봐야 할지 전반적으로 헷갈렸다. 만일 회의 중에 A를 확인하기 위해 대시보드를 열어본 상황을 가정했을 때, &#39;A를 어떻게 봐야하지..&#39; 두 눈이 흔들리고 길을 잃어버리는 혼돈의 상황이 상상되었다.</p>
<p> 그래서 참고용 지표는 상세 대시보드로 빼고, 일별로 변동이 크지 않은 수치는 아예 제외했다. 최근 회의 시간을 떠올리며 자주 소비된 지표만 메인 대시보드에 남겨두었다. 그리고 개선된 대시보드를 팀원들에게 공유하며 최근에 진행한 액션을 예로 들어 A 지표가 이전과 대비해서 몇 퍼센트 개선된 것인지 함께 설명했다.</p>
<blockquote>
<p>전환율 A가 20%p 개선되었는데, 이는 매일 서비스에 진입하는 사람이 10만 명이라고 가정할 때 매일 전환되는 사람이 2만 명 늘어나는 효과예요.</p>
</blockquote>
<p> 중요하지 않은 지표는 제외하고, 메인 지표를 위주로 공유했을 때 팀원들이 더 잘 이해한다는 느낌을 받았다. (=후속 질문을 더 많이 받았다) </p>
<p> 이렇게 핵심 지표로 요약하는 방식은 여러 액션의 성과를 비교하기에도 좋았다. 매번 새로운 지표로 성과를 측정하는 것이 아니라, 일관성 있는 뷰로 바라볼 때 &#39;우리 팀의 방향은 A를 높이는 것인데, 예전 실험보다 이번 실험의 성과가 더 좋네요&#39;라고 바로 비교할 수 있기 때문이다. </p>
<p> 데이터를 파묘하다보면 이렇게도 볼 수 있고 저렇게도 볼 수 있는 것이 사실이다. 그렇지만 한층 더 깊게 분석하는 작업은 메인 지표가 기대와 달리 움직이지 않을 때 진행되어도 늦지 않은 것 같다. </p>
<p> </br></br></p>
<h2 id="2-일-단위로-이만큼이면-주-단위로는-얼만큼이죠">2. 일 단위로 이만큼이면, 주 단위로는 얼만큼이죠?</h2>
<p>타깃 대상자가 앞으로 얼마나 늘어날지 대시보드로 확인하고 싶다는 요청이었다. 큰 폭으로 늘어나는 시점을 예상하고 미리 대응하기 위해서다. 먼저 daily로 지표를 그려서 회의에 가져가니, 주 단위 월 단위의 변화는 어떠할지 질문을 받았다. 대시보드에 표를 추가로 그려야하나 고민을 하고 있던 찰나, 마침 옆자리 동료분이 태블로에 매개변수 기능이 있다는 것을 알려주셨다. </p>
<p>매개변수는 리모컨 같은 도구다. </p>
<blockquote>
<ul>
<li>집계 기준을 여러 가지로 세팅이 가능하다. (ex. daily/weekly/monthly, pv/uv, 연령별/성별 등등 group by 변수를 조정할 수 있다.)</li>
<li>집계 일자를 조정할 수 있다. </li>
<li>하이라이트를 할 수 있다. (ex. 건수가 1만이 넘었을 때 하이라이트)</li>
</ul>
</blockquote>
<p>매개변수는 항상 필요한가? 그건 아니다. 하지만 자주 받는 질문을 리스트업하고 대시보드에 추가 반영해 놓으면, 회의할 때 당황하지 않고 바로 필터를 조정해서 수치를 확인하고 의사결정할 수 있다는 장점이 있는 것 같다. </p>
<p></br></br></p>
<h1 id="마무리하며">마무리하며</h1>
<p>지금 대시보드가 필요한가? 라는 질문에 답하려면, 지속적으로 모니터링할 중요한 지표가 무엇일지 우선 파악해야한다. 이 문제는 내가 혼자 고민하는 것이 아니라 대시보드를 요청한 사람과 여러번 논의해서 결정해야하는 것 같다. </p>
<p>만일 내가 어떠한 형태의 대시보드를 요청받아도 2시간 이내에 무조건 만들 수 있는 능력자라면 이런 고민을 하지 않고 일단 다 만들어둘 수도 있었을 것 같다. 하지만 나는 그런 능력을 가지고 있지 않고 대시보드를 만드는 데 시간이 꽤 걸린다. 대시보드에 n시간을 더 투자하는 만큼 다른 업무를 할 수 있는 시간이 n시간이 줄어든다. 그래서 중요한 것이 무엇인지 요약하고, 모르는 것은 물어보고 파악하면서 최대한 시간을 효율적으로 쓰는 연습도 이제 해야할 것 같다. </p>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/cf724e39-8200-406a-a54c-4824a7409e23/image.png" alt=""></p>
<p>_출처 : <a href="https://youtu.be/IqF_TTwJ08s?si=r3wIyX-U3fLHyYHz&amp;t=1898">미생 유튜브 클립</a> _</p>
<p>이 글을 쓰면서 미생의 한 장면이 생각났다..ㅋㅋ 철강팀에서 강대리가 장백기에게 문장 줄이기 과제를 주는 장면인데 업무를 할 때도 문득 생각난다. 문장 줄이기는 정말 정말 어렵다. 내용을 깊이 이해하고 누군가를 이해시킬 수 있도록 설명할 수 있는 단계까지 도달해야, 전달이 잘 되는 요약을 해낼 수 있는 것 같다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[글또 10기를 시작하며]]></title>
            <link>https://velog.io/@hong_journey/%EA%B8%80%EB%98%90-10%EA%B8%B0%EB%A5%BC-%EC%8B%9C%EC%9E%91%ED%95%98%EB%A9%B0</link>
            <guid>https://velog.io/@hong_journey/%EA%B8%80%EB%98%90-10%EA%B8%B0%EB%A5%BC-%EC%8B%9C%EC%9E%91%ED%95%98%EB%A9%B0</guid>
            <pubDate>Sun, 13 Oct 2024 13:56:50 GMT</pubDate>
            <description><![CDATA[<p>글또 8기가 끝나고 이제는 부담을 덜어내고 여유를 가지고 블로그에 글을 써보자고 생각했는데, 남아있는 한 줌의 부담마저 모두 벗어던지고 글쓰기를 1도 안 할줄은 몰랐다. 블로그 글쓰기를 꼭 해야만 하나? 그건 아니지만 이렇게 되면 공부까지 영영 안 할 것 같아서 글또에 다시 참여했다. </p>
<p>지난 8기를 시작할 때는 취준을 하고 있었는데 10기를 시작하는 지금은 어느덧 일을 시작한 지 2년 차가 되었다. 글또를 하면서 2주간 글감을 고민하다 일요일이 되어서야 글쓰기를 시작했던 기억을 떠올리면 이번에도 분명 무한 고통 지옥이 예상되지만, 글또를 통해 얻은 것 역시 많기 때문에 이번 활동도 기대가 된다.</p>
</br>

<h1 id="이때에만-할-수-있는-생각-지금이라서-해볼-수-있는-시도가-있다">이때에만 할 수 있는 생각, 지금이라서 해볼 수 있는 시도가 있다</h1>
<p>글또에 참여하며 가장 좋았던 점은 내가 당시에 어떤 고민을 했는지, 지금과는 무엇이 다른지 비교해 볼 수 있다는 것이다. 나는 회고를 거의 하지 않지만 (게을러서..) 그래도 가끔 메모장에 지금 하고 있는 고민을 적어둔다. 그러다 6개월 전에 작성했던 메모를 들여다보기도 하는데, 거기에 써둔 고민 중 하나라도 달라지거나 나아진 점이 있다면 &#39;그럼에도 불구하고 조금 나아지고 있다&#39;라고 동기부여를 얻는 것 같다.</p>
<p>취준을 할 때도 &#39;아직 내가 일한 경험도 없는데 뭘 써야 하나&#39; 싶다가도, 당시 주요 일과가 포폴 고치기였기 때문에 프로젝트를 정리하며 발견한 문제점들을 &#39;지금 다시 프로젝트를 한다면 어떻게 개선할지&#39;에 대해 글을 써보기로 했다. 글또를 하면서 프로젝트 회고 글을 작성한 덕분에 면접의 기회가 더 많이 생겼고, 결국 원하는 곳에 취업도 할 수 있었다고 생각한다. </p>
</br>

<h1 id="요즘-하는-고민">요즘 하는 고민</h1>
<p>아직 업무 경험이 많지 않기 때문에 이번 글또 기수에서도 수박 겉핥는 느낌이 낭낭한 글을 작성하게 되겠지만, 이것 또한 지금의 내가 할 수 있는 진지한 고민이기 때문에 되도록 가벼운 마음으로 기록하고 싶다. </p>
<h2 id="q1-이거-좋은데-왜-좋지">Q1. 이거 좋은데.. 왜 좋지?</h2>
<p>일하다 보면 &#39;이 사람의 분석 결과는 뭔가 다르다&#39;는 생각이 자주 들 때가 있는데, 내가 왜 이걸 좋다고 느낀 것인지 말로 표현하기 어려운 때가 종종 있다. 그래서 퇴근 후 &#39;이게 왜 좋았나&#39;를 일반화하여 짧게 메모해 두고 며칠 뒤 나의 작업에도 조금씩 적용해 보기도 하는데, 이렇게 따라 하는 과정이 재미있기도 하고 내가 몰랐던 내용을 체화할 수 있는 방법이기도 한 것 같다. 예를 들어 &quot;좋은 시각화 예시를 정리하고 업무에 적용하기&quot; 같은 글을 작성하려고 한다. </p>
<h2 id="q2-이거-별론데-왜-별로지">Q2. 이거 별론데.. 왜 별로지?</h2>
<p>무엇을 시도 했을 때 결과가 괜찮았는지 혹은 별로였는지를 도식화하고, 이를 미래에 적용하는 글을 써보고 싶다. 히스토리를 뒤지다가 슬랙 검색을 통해 6개월, 3개월전 내가 작성한 스레드를 발견할 때가 있는데 왠지 모르게 구리다고 느낀 적이 많다. 망한 분석 결과가 장기적으로 어떤 악영향을 줬는지, 별로였다면 왜 별로였는지 돌아보고 지금 한다면 어떤식으로 다르게 해볼 수 있을지 회고하는 글을 작성해보고 싶다. </p>
</br>

<h1 id="마무리하며">마무리하며</h1>
<p>위에 거창하게 다짐을 해보았지만 사실 무엇보다 이루고 싶은 목표는 완주다.. 이번 글또 10기가 마지막 기수라고 해서 더욱더 중도 포기만은 피하고 싶다. 그러기 위해선 미리미리 작성하는 습관을 들여야하는데.. 주말에 유튜브 보는 것을 좀 줄이고 앉아서 뭐라도 써보는 습관을 들여야겠다. 화이팅!!</p>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/06331303-fdc8-4888-9a63-a8b9b12ca538/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[글또 8기 완주]]></title>
            <link>https://velog.io/@hong_journey/%EA%B8%80%EB%98%90-8%EA%B8%B0-%EC%99%84%EC%A3%BC</link>
            <guid>https://velog.io/@hong_journey/%EA%B8%80%EB%98%90-8%EA%B8%B0-%EC%99%84%EC%A3%BC</guid>
            <pubDate>Sun, 16 Jul 2023 14:57:40 GMT</pubDate>
            <description><![CDATA[<h2 id="1-겨우겨우-완주했으나-실패한-것">1. 겨우겨우 완주했으나 실패한 것</h2>
<p>글또 7기와 비교해서 이번 기수에서 스스로에게 크고 작은 아쉬운점이 많았던 것 같다. &lt;<a href="https://velog.io/@hong_journey/%EA%B8%80%EB%98%90-8%EA%B8%B0%EB%A5%BC-%EC%8B%9C%EC%9E%91%ED%95%98%EB%A9%B0">글또 8기를 시작하며</a>&gt; 글을 읽어보니 더 그런 생각이 든다. 8기를 하기 전과 현재의 나는 어떤 부분이 달라졌는가. 우선 환경이 많이 달라졌다. 하지만 변화한 환경에 비해 내가 계획했던 많은 것들을 이루지 못했다. </p>
<p>1년 넘게 취준생으로 살다가 대학원을 알아보던 와중, 면접에 합격하여 일을 배우기 시작했다. 감사한 마음을 가득 안고 글또 활동을 더 열심히 해야겠다고 생각했다. 그동안은 취준생으로서 내가 얻은것이 많았는데, 드디어 글또를 통해 누군가에게 기여할 수 있겠다는 기대감이 있었기 때문이다. </p>
<p>그런데 결과적으로 목적을 달성하는 데 실패했다. </p>
<p>글또를 통해 분석가로 일하시는 분들을 만나면서 그분들이 어떤 일을 하시는지, 평소에는 어떤 고민을 하고 계시는지 알 수 있었다. 그런 말을 듣다보니 분석가의 역할이 분석에서 그치지 않을 가능성이 매우 클 것이라 생각했다. 그래서 로그 정의, 집계 방식 결정, 분석 환경 세팅 등의 업무를 할 수 있어야겠다고 판단하여 데이터엔지니어링 공부를 계획… 했으나 제대로 해보지 못했다. 내가 게을렀기 때문이기도 하지만.. 생각이 조금 달라진 것도 있다. 데이터 분석가가 엔지니어링 스킬도 있으면 물론 좋겠지만, 더 중요한 것은 문제정의하는 역량이라는 것이다. 분석가는 배포 혹은 실험 이후에 벌어지는 다양한 상황들을 보고 가설을 세워 확인해야하긴한데, 이러한 업무가 한꺼번에 몰려오다보면 때로는 여러 문제들중 가장 임팩트가 높은 문제를 선택하는 등 우선순위를 관리하는 역량도 필요해보인다. 그런데 이게 말이 쉽지.. 머리로는 이해되는데 구체적을 어떻게 그런 역량을 키울 수 있는지 깊게 와닿지 않긴 하다. (그래서 다른 분들이 이전에 분석하고 공유해놓으신 자료들을 읽으면서, 이거는 왜 이렇게 했는지? 다른 방법 A도 있을 것 같은데 왜 택하지 않았는지? 질문하면서 그런 역량을 연마해나갈 수 있지 않을까.. 추측을 해봄)</p>
<h2 id="2-그럼에도-성공한-것">2. 그럼에도 성공한 것</h2>
<p>이력서를 업데이트하다가 이전 프젝에서의 문제점을 발견하고 개선사항을 글로 작성했다. 이전까지 포장지만 바꾸다가 내용을 바꾼 것은 이 때가 처음이었는데, 시도해보길 잘했다는 생각이 들었다. 아무리 만족스로운 프로젝트더라도 문제는 반드시 존재하기 마련이니, 시간이 지나 개선할 포인트를 찾아 고민하는 과정이 재밌다는 것을 느꼈다. 앞으로도 계약 또는 정해진 기간동안 1회성 업무를 진행하는 일을 선택하기보단, 서비스 배포 사이클에 여러번 참여하며 서비스 개선에 기여하는 일을 하고싶단 생각이 들었다. </p>
<p>커피챗으로 좋은 인연을 만든 것도 감사하다. 글또에서 만난 분들의 이야기를 들으며, 나는 분석가가 왜 되고 싶고 어떤 일에 흥미를 느끼는가.. 를 깊게 고민할 수 있었다. 입사 후 첫 3개월동안 정신없는 시간들을 보내면서 공부나 회고 등 개인적인 시간을 갖지 못했는데, 글또 커피챗을 통해 잠시 하던일을 멈추고 내가 지금 하고 있는 이게 맞나? 라는 고민을 할 수 있었던 것 같아 커피챗 글또분들에게 굉장히 감사하다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[sql] not exists와 not in 차이]]></title>
            <link>https://velog.io/@hong_journey/sql-not-exists%EC%99%80-not-in-%EC%B0%A8%EC%9D%B4</link>
            <guid>https://velog.io/@hong_journey/sql-not-exists%EC%99%80-not-in-%EC%B0%A8%EC%9D%B4</guid>
            <pubDate>Sun, 02 Jul 2023 14:54:38 GMT</pubDate>
            <description><![CDATA[<p>업무중 이전 분석 자료들을 찾아보다가 exists 구문을 알게되었다. </p>
<p>전체 앱로그 유저 중에서 table a에 특정 기간동안에 기록된 유저를 제외하는 목적으로 NOT EXISTS 가 쓰이고 있었다. 언뜻 이해했을 때는 NOT IN 과 유사해보였으나 다른 부분이 있었다. exists, not exists 는 언제 쓰이는지 예시를 확인하고 in, not in 과 무엇이 다른지 알아보았다. </p>
</br>
</br>

<h1 id="exists">Exists?</h1>
<p>exists의 기본적인 형태는 다음과 같다. </p>
<pre><code class="language-sql">-- table_1 중에서 table_2 에도 있는 row만 추출하기
SELECT 
    column1
FROM 
    table_1
WHERE -- 메인쿼리 조건문
    EXISTS ( SELECT 
                1 
            FROM 
                table_2 
            WHERE 
                column_2 = table_1.column_1);</code></pre>
<p>exists 뒤에 서브쿼리가 따라오는 구조다. 서브쿼리의 조건문에는 메인쿼리의 table_1.column 도 포함된다. 보통 이런 구조를 correlated subquery라 부른다고 한다. </p>
<p>exists 뒤 서브쿼리에서 적어도 하나의 행이 리턴되면, 메인쿼리의 조건문은 true을 반환하고, 서브쿼리에서 어떠한 행도 리턴하지 못하면 메인쿼리 조건문은 false를 리턴한다. </p>
<p>이 때 주의할 것이, 서브쿼리가 NULL을 리턴하더라도 메인쿼리의 조건문 결과는 true이라는 것이다. (NULL이라도 행을 리턴하긴 했으므로)</p>
<p>참고로 서브쿼리의 select 문은 어떠한 형태라도 상관이 없다. (행이 하나라도 존재하는지 여부만 중요)
아래 세 가지 쿼리의 결과는 동일하다.</p>
<ul>
<li><code>SELECT column1 FROM t1 WHERE EXISTS (SELECT * FROM t2);</code></li>
<li><code>SELECT column1 FROM t1 WHERE EXISTS (SELECT 1 FROM t2);</code></li>
<li><code>SELECT column1 FROM t1 WHERE EXISTS (SELECT column1 FROM t2);</code></li>
</ul>
</br>
</br>

<h1 id="사용-예시">사용 예시</h1>
<p>총 두가지 테이블이 있다고 하자. </p>
<ul>
<li>customer 테이블: 고객 id, 고객 이름</li>
<li>payment 테이블 : 구매 id, 고객 id, 수량</li>
</ul>
<p>전체 고객 중 10개 이상의 수량을 구매해본 이력이 있는 고객을 찾는다고 가정하자. </p>
<p>추출하는 방법은 여러가지가 가능하다. 고객 정보 테이블을 기준으로 구매 이력 테이블을 left join 하는 방법도 있고, 아래처럼 exists를 쓸 수도 있다.</p>
<pre><code class="language-sql">SELECT name
FROM customer c
WHERE EXISTS 
    (SELECT 1
     FROM payment p
     WHERE p.customer_id = c.customer_id -- payment 테이블에 기록된 고객인가
       AND amount &gt;= 10 ) -- 그리고 이 때 구매 수량이 10개 이상인가
ORDER BY name;</code></pre>
<p>메인쿼리에서 customer 테이블의 각 name 마다, 서브쿼리 결과가 true인지, false 인지 여부를 조회한다. </p>
<p>반대로 수량을 10개 이상으로 구매해본적 없는 고객을 추출하려면? 아래와 같이 <code>not exists</code> 를 이용하여 찾을 수 있다. </p>
<pre><code class="language-sql">SELECT name
FROM customer c
WHERE NOT EXISTS -- 메인쿼리 조건문
    (SELECT 1
     FROM payment p
     WHERE p.customer_id = c.customer_id
       AND amount &gt;= 10 )
ORDER BY name;</code></pre>
<ul>
<li>만약 서브쿼리가 어떠한 row도 리턴하지 않는다면? 메인쿼리의 조건문(where not exists ~)은 true를 리턴한다.</li>
</ul>
</br>
</br>

<h1 id="not-exists와-not-in의-차이점">not exists와 not in의 차이점</h1>
<p>아래 쿼리처럼 서브쿼리가 NULL을 리턴하면 exists는 true를 리턴한다.  </p>
<pre><code class="language-sql">-- 아래 쿼리의 결과는 select name from customer 와 동일하다.
SELECT
    name
FROM
    customer
WHERE
    EXISTS( SELECT NULL )
;
</code></pre>
<p>서브쿼리에 NULL이 리턴되는 상황에서 <code>NOT IN</code> vs <code>NOT EXISTS</code> 사이 차이가 발생한다.<br>예를 들어 아래와 같은 테이블이 있다고 하자. </p>
<pre><code class="language-sql">with customer as (
    select 100 as customer_id, &#39;a&#39; as name
    union all
    select 200 as customer_id, &#39;b&#39; as name
    union all
    select 300 as customer_id, &#39;c&#39; as name
), payment as (
    select 100 as customer_id, 15 as amount
    union all
    select 300 as customer_id, 7 as amount
    union all
    select NULL as customer_id, 10 as amount -- customer_id NULL 존재
)</code></pre>
<p>customer 테이블에 기록된 고객 중 구매 기록이 없는 고객의 customer_id를 찾고자 한다. 구매기록은 payment 테이블에 기록되어 있다.</p>
<blockquote>
<p><strong>customer</strong></p>
</blockquote>
<ul>
<li>customer_id</li>
<li>name<blockquote>
<p><strong>payment</strong></p>
</blockquote>
</li>
<li>customer_id</li>
<li>amount</li>
</ul>
<p>간단하게 NOT IN을 쓰면 되지 않을까? </p>
<pre><code class="language-sql">-- 1) not in 을 사용한 경우
select name
from customer
where customer_id not in (
                select customer_id
                from payment
);</code></pre>
<p>그런데 위 쿼리를 실행하면 아무런 행도 추출되지 않는다. 
payment 테이블의 customer_id 에는 NULL값이 포함되어 있기 때문이다. NOT IN 이하 서브쿼리가 먼저 실행되는데 서브퀴리 결과물 중 NULL이 포함되어 있다. 그래서 NOT IN (SELECT NULL)의 조건문의 결과 0개의 행이 추출된다. </p>
<pre><code class="language-sql">-- 2) not exists 를 사용한 경우
select name
from customer
where not exists (
                select 1
                from payment
                where payment.customer_id = customer.customer_id
);</code></pre>
<p>반면 not exists 를 사용한 경우, name=&#39;b&#39; 인 행이 추출된다. 
not exists는 customer 테이블의 customer_id마다 payment 테이블에 존재하는지 여부를 조회한다. 따라서 payment.customer_id에 NULL이 있든 없든 동일한 결과가 추출된다. </p>
<hr>
<p>참고 자료 : </p>
<ul>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/exists-and-not-exists-subqueries.html">https://dev.mysql.com/doc/refman/8.0/en/exists-and-not-exists-subqueries.html</a></li>
<li><a href="https://www.postgresqltutorial.com/postgresql-tutorial/postgresql-exists/">https://www.postgresqltutorial.com/postgresql-tutorial/postgresql-exists/</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[로그 정의 업무 회고]]></title>
            <link>https://velog.io/@hong_journey/%EB%A1%9C%EA%B7%B8-%EC%A0%95%EC%9D%98-%EC%97%85%EB%AC%B4-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@hong_journey/%EB%A1%9C%EA%B7%B8-%EC%A0%95%EC%9D%98-%EC%97%85%EB%AC%B4-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sun, 04 Jun 2023 14:52:07 GMT</pubDate>
            <description><![CDATA[<p>입사한지 두 달이 되어간다. 실시간으로 적응해나가고 있다는 안도감을 느끼는 동시에, 가까운 미래에 무언가 큰 일이 터질 것 같은 불안함도 커져가고 있다. 사건의 발단은 한 슬랙 메세지였다. 특정 화면에 남는 파라미터에 대한 문의였는데, 생전 처음 본 듯한 내용이었다. 분명 지난 달에 내가 확인했던 로그 데이터였는데..! 심지어 ‘이걸 내가 로그 QA 했다고?’ 하는 의심마저 들었다. 한주에 무엇을 했는지 돌아보지 않고 쉬다가 주말이 끝나다보니 깊게 고민하지 않는 일은 바로 기억속에서 사라지는 것 같다. </p>
<p>그런 의미로 최근 수행한 로깅 업무를 돌아보는 글을 쓰고자 한다. 하나의 제품을 출시하기 위해 디자이너, 개발자, PO 등이 핑퐁 논의를 하고, 그 결과 만들어진 화면에서 로그를 정의하면 되는 일이기에 처음에는 쉬워보였다. 하지만 막상 해보니, 로그 정의는 고민이 꼬리에 꼬리를 무는 어려운 일이었다. 로깅을 하면서 어떤 어려움이 있었고, 무엇을 배웠는지 돌아보았다. </p>
<p></br></br></p>
<h2 id="1-어디까지-남겨야하나">1. 어디까지 남겨야하나</h2>
<p>나는 직무 특성상 여러 팀의 로그 QA와 로그 정의 업무를 하고 있다. 그렇다보니 나는 제품을 만드는 메이커가 아닐뿐더러, 로그 데이터를 분석하는 주체라기에도 애매하다. 그래서 로깅할 때 어떤 목적으로 로그를 정의해야할지, 내가 정의한 로그를 미래에 어떻게 활용할 수 있을지 가늠하는 것이 가장 어려웠다. 내가 로깅 경험이 부족해서 생긴 문제일 수 있다. 하지만 시간이 지나면 저절로 해결되는 문제일까? 그렇지도 않아보인다. </p>
<p>로그 정의처럼 정답이 없는 문제일수록 팀원(데이터 분석가, PO, 개발자 등)들에게 의견을 물어보고 결정하는 것이 더 효율적인 방법 같다. 어려워서 하루 넘게 붙들고 있기보단, <strong>내가 어디에서 막혔는지 정확히 파악하고 같이 일하는 팀원 혹은 위클리 미팅을 통해서 도움을 받아서 했더라면</strong> 여러날에 걸쳐 마친 업무가 하루만에 끝났을 수도 있겠다 싶었다. 이렇게 의견을 구하는 것 자체가 중요한 스킬이라는 생각이 들고(하지만 나는 여전히 부족한..) 내가 생각하지 못했던 맥락을 빠르게 이해해 우선순위를 파악하기 좋은 것 같다. </p>
<p></br></br></p>
<h2 id="2-이것까지-남겨야하나">2. 이것까지 남겨야하나?</h2>
<p>화면에 진입하는 유저가 어떤 행동을 하느냐에 따라 다양한 이벤트 로그를 정의할 수 있다. 예를 들어 click, impression, scroll 등을 남길 수 있다. 그 중 impression 로그를 남기는 목적은 무엇일까? impression 로그는 일반적으로 어떤 요소가 유저에게 노출되었는지 구분하기 위해 쓰인다. 예를 들어, 배너 A가 유저마다 노출 여부가 다른 경우 특정 유저에게 해당 배너가 노출되었는지 여부를 남길 때도 쓰이고, 유저가 여러 항목이 포함된 리스트에서 한 아이템을 클릭하기까지 어떤 아이템들이 노출되었는지 로깅할 수도 있다. </p>
<p>이것까지 로그를 남겨야햐나, 말아야하나 고민이 들 때는 직접 실행해볼 수 있는 테스트 스킴을 개발자에게 요청해서 확인하면 좋다. </p>
<blockquote>
<p>스킴이란?</p>
</blockquote>
<ul>
<li>스킴은 특정 페이지로 바로 이동할 수 있는 url이다.</li>
<li>스킴을 통한 이동은 크게 1. 네이티브 앱 화면으로 이동과 2. 웹뷰로 만든 화면으로 이동으로 나눌 수 있다.</li>
<li>개발된 기능을 테스트할 때 쓰이고, 푸시를 통해 사용자를 진입시킬 때도 스킴이 쓰인다.</li>
</ul>
<p>프레이머나 피그마를 통해 디자인으로 보고 바로 로깅하기보단, 스킴을 통해 직접 퍼널을 타보며 한번 더 확인하는 작업이 필요하다. 최근에는 스킴으로 앱을 실행해보다가 내가 빠트린 로그를 발견한적이 있다. 퍼널 중간 즈음에 뒤로가기를 눌렀는데 예상했던 것과 다른 화면으로 랜딩되었던 것이다. </p>
<p>스킴으로 앱을 직접 실행하다보면 로깅 시점을 파악하기도 편하다. 예를 들어 유저가 생년월일과 주소를 차례대로 입력하는 화면이 있다고 할 때, 주소 입력 단계를 impression 로깅한다고 하자. 그럼 로그를 남겨야하는 시점은 언제일까? 유저가 주소 입력 화면을 만났을 때가 될 수도 있고 유저가 주소를 입력하기 시작하는 시점일 수도 있다. 만약 “유저가 주소 정보를 입력하다가 거부감을 느껴 이탈한다” 라는 가설을 확인해보고 싶다면 유저가 입력을 시작하는 시점에 로그를 남길 수도 있겠지만 그러한 원인 파악이 중요하지 않다면 유저가 해당 화면을 보는 시점에만 로그를 남겨도 될 것이다. 최종적으로 내가 정의한 시점대로 로그를 남기는 것이 가능할지 개발자와 소통하며 확인해야하고, 이때 논의한 내용은 추후 로그가 예상한 시점대로 들어오는지 확인하는 QA 과정에서 중요하다. </p>
<p></br></br></p>
<h2 id="3-마무리하며">3. 마무리하며</h2>
<p>로깅을 하다가 이전 로그 중 비슷한 제품을 찾아 참고하기도 한다. (아니 거의 매일 참고한다..) 간단하게는 로그 이름을 어떻게 정의했는지부터 어떤 이벤트 로그를 남겼는지 등을 참고하고 있다. 과거에 누군가 로깅한 자료들을 보면, 로깅 방식과 로그 종류의 장단점을 배울 수 있다. 로그 정의 경험이 늘어날수록 로깅 정답을 더 많이 알게 된다기보단, 특정 로그를 포함할지 말지,에 대해 장단점을 폭넓게 이야기할 수 있게 되는 것 같다. 그래서 나의 가까운 목표는 로그 정의 경험을 쌓으며 해당 로그를 추가하면 어떤 분석에 활용할 수 있는지 설명하고, 각 로깅 방식마다 장단점을 제시할 수 있는 사람이 되는 것이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[입사 2주 기록]]></title>
            <link>https://velog.io/@hong_journey/%EC%9E%85%EC%82%AC-2%EC%A3%BC-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@hong_journey/%EC%9E%85%EC%82%AC-2%EC%A3%BC-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sun, 23 Apr 2023 14:59:17 GMT</pubDate>
            <description><![CDATA[<p>좋은 기회가 생겨 업무를 배우고 있다. 입사 2주 차가 되어 느낀 바로는 주된 업무가 데이터 분석은 아니고, 로그 데이터를 QA 하며 데이터 품질을 효율적으로 관리하는 데 기여할 수 있는 직무 같다. </p>
<p>데이터 관련 업무 경험은 이곳이 처음이라, 입사일을 앞두고 막막함과 경직됨을 느꼈다. 이제까지의 경험을 미루어보았을 때, 나는 보고 듣는 것보다 직접 부딪히고 적용할 때 더 많이 배우는 편이라, 입사 초기에는 무엇이든 부딪히고 해결해 나가며 유연하게 적응하는 것이 목표였다. </p>
<p>2주가 지난 지금은 내가 무언가를 해결한다.. 는 먼 이야기 같고, 대신 나의 역량이 얼마나 부족한가를 매일매일 느끼는 중이다. 2주 차에는 QA 업무를 배우고 조금씩 해보기 시작했는데, 나에게 병목이 발생하면 안 된다는 생각에 급하게 QA를 하다 보니 문득 생각 없이 주어진 일을 하고 있는 내 모습을 발견했다. 내가 그동안 어떻게 QA를 하고 있는지 실수했던 것 위주로 점검해 보고, 앞으로의 수습 3개월 동안의 목표를 세워 보았다. </p>
</br>
</br>

<h1 id="나의-qa-일지">나의 QA 일지</h1>
<h2 id="1-누락된-로그와-파라미터-점검">1. 누락된 로그와 파라미터 점검</h2>
<p>사내 제품의 각 퍼널마다 어떤 로그와 파라미터가 남는지 파악 가능한 저장소가 있다. 이를 실시간으로 쌓이는 로그와 비교해 가며 누락된 로그와 파라미터가 있는지, 파라미터값은 예상대로 찍히고 있는지 확인한다. 만약 누락되는 로그가 있거나 파라미터 값이 정의한 대로 기록되지 않는다면, 이어지는 분석 업무에 큰 영향을 줄 수 있으므로 꼼꼼히 확인해야 하는 중요한 작업이다. </p>
<h2 id="2-순서대로-찍히는지-점검">2. 순서대로 찍히는지 점검</h2>
<p>퍼널마다 순서대로 로그가 찍히고 있는지 확인한다. 예를 들어, 노출 이벤트 다음 클릭 이벤트가 찍혀야 하는데, 로그가 찍힌 순서가 반대라면 문제가 된다. 이때 주의해야 할 점은 정렬 기준인 시간 필드인데, 이게 프로세스 시간인지 이벤트 시간인지 구분해야 한다. 클라이언트 상에서 로그가 생성된 시간을 이벤트 시간이라 하고, 서버가 처리하는 시간을 프로세스 시간이라고 한다. 데이터 분석은 이벤트 시간을 기준으로 하기 때문에, 시간 순서대로 확인하려면 (프로세스 시간이 아니라) 이벤트 시간을 기준으로 정렬해야 한다. </p>
<h2 id="3-예상된-시나리오대로-찍히고-있는지-점검">3. 예상된 시나리오대로 찍히고 있는지 점검</h2>
<p>처음에는 퍼널을 정의하고, 테스트 기기를 사용해 일반적인 방식으로 퍼널을 타서 로그가 예상대로 찍히는지 확인하면 되는 줄 알았다. 그런데 그게 아니라, 어떤 시나리오로 퍼널을 타는지에 따라 유입 파라미터값이 다를 수 있고 특정 로그가 아예 안 찍힐 수도 있다는 것을 알게 되었다. 그래서 QA하기 전에 미리 여러 가지 가능성을 고려하여 시나리오를 짜고, 실제로 예상대로 로그가 쌓이고 있는지 또는 로그가 중복으로 발생하진 않는지 확인해야한다. 시나리오에 따라 파라미터값이 다르게 찍힐 수도 있기 때문에, QA를 시작하기 전에 시나리오별 파라미터 예상값을 기록해 두는 것이 QA 시간을 줄여준다.  </p>
</br>
</br>


<h1 id="앞으로의-목표">앞으로의 목표</h1>
<p>4월 목표 : </p>
<ol>
<li><p>업무툴(kibana, jira, hue, 사내 툴 등)에 대한 이해를 높이고 그 밖의 새로운 기술에 대한 빠른 학습과 활용 감각 키워나가기.</p>
</li>
<li><p>담당 서비스의 퍼널을 파악하고 로그 QA 참여</p>
<ul>
<li>로그와 파라미터가 어떤 목적(ex. 분석, 실험)으로 활용되는지 파악</li>
</ul>
</li>
</ol>
<p>5월 목표:</p>
<ol>
<li>담당 서비스의 로그 정의<ul>
<li>로그 정의 후 활용되는 과정 추적</li>
<li>로그 정의 단계에서 다른 대안이 없었는지 논의</li>
</ul>
</li>
<li>QA 진행 중 실수 문서화<ul>
<li>빠트린 작업이나 실수를 방지하고자 기록</li>
<li>로그 모니터링 방식이 상황마다 무엇이 다른지 파악하고, 각 방식의 장단점 설명하기</li>
</ul>
</li>
</ol>
<p>6월 목표:</p>
<ol>
<li>로그 모니터링 단계에서 병목을 확인하고 효율적으로 개선할 방안 제시</li>
<li>로그 관련 사내 툴 개선 아이디어 도출</li>
</ol>
</br>
</br>

<h1 id="마무리하며">마무리하며</h1>
<p>입사 2주가 지나고 있는데 현재 내가 무슨 일을 하는지, 팀에게 어떤 기여를 할 수 있을지 여전히 명확하지 않은 막막한 상황이다. 아직 업무 이해도 부족하고 경험도 부족하지만, 좌절하고 아무것도 안 하는 것보단 현재 상황을 스냅샷으로 남겨 앞으로 개선해 나가는 것이 나은 선택이라는 생각이 들었다. 입사 3개월이 지난 시점에 오늘 작성한 부분에서 어떤 접근방식이 잘못되었고, 어떻게 개선했는지 회고하는 글을 써야겠다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[<데이터 문해력> 읽고 인프런 분석해보기]]></title>
            <link>https://velog.io/@hong_journey/%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%AC%B8%ED%95%B4%EB%A0%A5-%EC%9D%BD%EA%B3%A0-%EC%9D%B8%ED%94%84%EB%9F%B0-%EB%B6%84%EC%84%9D%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@hong_journey/%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%AC%B8%ED%95%B4%EB%A0%A5-%EC%9D%BD%EA%B3%A0-%EC%9D%B8%ED%94%84%EB%9F%B0-%EB%B6%84%EC%84%9D%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sun, 09 Apr 2023 14:55:50 GMT</pubDate>
            <description><![CDATA[<p>이전에 프로젝트를 마무리하고 팀원들에게 피드백을 구한 적이 있는데, HTTP에 대한 지식이 많이 부족하다는 것을 알게됐다. 이때 부족한 지식을 빠르게 보완하는 데 큰 도움을 얻은 플랫폼이 인프런이다. 인프런은 양질의 콘텐츠 뿐만 아니라, 멘토링이나 질문 답변과 같은 커뮤니티도 있어서 학습 과정에서 부족한 지식을 탐색해 관리하기 유용한 서비스인 것 같다. </p>
<p>인프런 강의들은 유튜브 영상처럼 마냥 자유로운 공유는 아니고, 평점이나 리뷰로 콘텐츠가 평가받는다. 나의 경우도 인프런에서 강의를 고를 때 리뷰나 평점을 많이 참고하는 편인데 유저가 강의를 클릭해 상세 페이지로 넘어가기 전, 평점/리뷰/수준레벨 정보에따라 유저가 어떻게 반응하는지 궁금해졌다. </p>
<p>그중 <strong>로드맵</strong> 서비스에서 유저 분석을 한다면, (가상이지만) 어떤 과정으로 진행할 수 있을지에 대해 고민하고 정리했다. </p>
</br>

<h1 id="로드맵은">로드맵은?</h1>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/ce67c7ca-387f-4048-9352-8c19d7f66c5d/image.png" alt=""></p>
<p>🔍 로드맵 페이지 : <a href="https://www.inflearn.com/roadmaps">https://www.inflearn.com/roadmaps</a></p>
<p>유저의 수준, 원하는 스킬과 커리어패스에 맞는 학습을 할 수 있게끔 강의를 로드맵 형태로 제공한다. </p>
<p>서비스의 타겟은?</p>
<ul>
<li>묶음 할인으로 결제하고 싶은 유저</li>
<li>자신에게 필요한 스킬이나 수준을 탐색해 학습하고 싶은 유저</li>
</ul>
<p>타켓 유저가 로드맵을 통해 기대했던 학습 경험을 하기 위해선, 유저 스스로 수준을 진단하고 이를 기반으로 로드맵에서 탐색하기 수월해야한다. 현재 로드맵 페이지를 통해 유저가 탐색할 수 있는 정보는 크게 5가지였다. </p>
<ul>
<li>검색 (원하는 스킬, 지식공유자명, 로드맵명 등)</li>
<li>분야 카테고리 선택 (ex. 개발 · 프로그래밍, 보안 · 네트워크, 데이터 사이언스)</li>
<li>시작 레벨 선택 (입문, 초급, 중급)</li>
<li>정렬 선택 (추천순, 학생수순, 공유순, 최신순, 오래된순)</li>
<li>콘텐츠 (로드맵명, 썸네일, 지식공유자명, 시작수준, 스킬, 강의수, 로드맵 참여자수 등)</li>
</ul>
</br>

<h1 id="문제-정의와-가설-수립">문제 정의와 가설 수립</h1>
<p>예를 들어 다음과 같은 문제를 정의한다고 가정하자.</p>
<h2 id="문제-정의--로드맵-썸네일-ctr이-감소했다">문제 정의 : 로드맵 썸네일 CTR이 감소했다.</h2>
<p>문제를 정의한 후에는 현황 파악 및 평가를 하고 원인을 분석한 다음 해결방안을 검토하는 순서로 진행한다. </p>
<blockquote>
<ul>
<li>원인 아이디어 1 : 로드맵 페이지로 유입되는 경로의 개수 (ex. 뉴스레터, 홈 배너 등등)가 줄어들었기 때문이다. </li>
<li>아이디어 부정 : 근데 유입 경로 개수의 문제가 아니라면? </li>
<li>원인 아이디어 2 : 유입 경로 개수를 늘려도 문제가 해결되지 않는다면, 유저에게 도달하지 않았을 가능성이 있는 건 아닌가? 그렇다면 유입 경로 개수는 관계가 없을지도 모른다. </li>
<li>아이디어 부정 : 유입 경로 개수는 충분하며, 퍼널별 전환율을 살펴보았을 때 유저에게 잘 도달하고 있다고 가정하고. 그럼에도 썸네일 CTR이 줄어든다면?</li>
<li>원인 아이디어 3 : 로드맵 썸네일 정보에 문제가 있나? (ex. 난이도를 가늠하기 어렵다)</li>
</ul>
</blockquote>
</br>

<h2 id="가설-수립">가설 수립</h2>
<p>위와 같은 과정으로 문제의 원인을 &quot;유저가 로드맵 난이도를 파악하기 어렵다&quot;이라고 분석했다고 하자. 이를 토대로 다음과 같은 가설을 수립해볼 수 있다.</p>
<ul>
<li>현재 로드맵 썸네일 예시
<img src="https://velog.velcdn.com/images/hong_journey/post/20ba258e-526c-460e-acaa-81958fa955b2/image.png" alt=""></li>
</ul>
<blockquote>
<p>가설 : 시작과 마지막 강의의 난이도 차이를 표시하면 로드맵 썸네일 클릭율(CTR)이 증가할 것이다. </p>
</blockquote>
<p>하나의 로드맵은 여러 난이도의 강의들로 구성되어 있다. 로드맵 썸네일에 &#39;시작 강의&#39;와 &#39;마지막 강의&#39;의 난이도 차이를 표시한다면, 로드맵 학습 이후의 실력 변화를 유저가 가늠할 수 있어, 로드맵을 탐색 하기 수월할 것이다. (ex. 입문~중급 강의 구성, only 중급 강의 구성)</p>
<p>샘플 크기가 충분하다면 AB 테스트를 진행해 볼 수 있다. 로드맵 썸네일에 &#39;난이도 차이&#39; 정보를 추가한 후, 클릭율이 개선되는지 확인하고자 한다. 가드레일 지표로는 로드맵 수강 전환율을 정의한다. (클릭율 개선의 목적은 로드맵 강의 매출 개선이다.) 클릭율을 개선하되, 수강 전환율이 떨어지면 실험을 중단한다. 가설 검증을 위해 다음과 같은 데이터가 필요하다. </p>
<ul>
<li>필요한 데이터</li>
</ul>
<table>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Type</th>
</tr>
</thead>
<tbody><tr>
<td>user_id</td>
<td>유저 아이디</td>
<td>uuid</td>
</tr>
<tr>
<td>event_type</td>
<td>search_click, field_click, level_click,<br> sort_click, thumbnail_click, roadmap_start 등 사용자 행동</td>
<td>string</td>
</tr>
<tr>
<td>event_value</td>
<td>검색했다면 키워드, 분야를 선택했다면 분야 키워드</td>
<td>string</td>
</tr>
<tr>
<td>created_at</td>
<td>해당 이벤트가 찍힌 날짜와 시간</td>
<td>datetime</td>
</tr>
<tr>
<td>roadmap_id</td>
<td>로드맵 콘텐츠 아이디</td>
<td>integer</td>
</tr>
</tbody></table>
<ul>
<li>데이터 예시</li>
</ul>
<table>
<thead>
<tr>
<th>user_id</th>
<th>event_type</th>
<th>event_value</th>
<th>created_at</th>
<th>roadmap_id</th>
</tr>
</thead>
<tbody><tr>
<td>224566</td>
<td>field_click</td>
<td>데이터 사이언스</td>
<td>2023-02-16 15:20</td>
<td>NULL</td>
</tr>
<tr>
<td>224566</td>
<td>sort_click</td>
<td>학생수 순</td>
<td>2023-02-16 15:29</td>
<td>NULL</td>
</tr>
<tr>
<td>224566</td>
<td>search_click</td>
<td>카일 스쿨</td>
<td>2023-02-16 16:30</td>
<td>NULL</td>
</tr>
<tr>
<td>224566</td>
<td>search_click</td>
<td>데이터 분석</td>
<td>2023-02-16 16:31</td>
<td>NULL</td>
</tr>
<tr>
<td>224566</td>
<td>thumbnail_click</td>
<td>NULL</td>
<td>2023-02-16 16:45</td>
<td>21</td>
</tr>
<tr>
<td>224566</td>
<td>roadmap_start</td>
<td>NULL</td>
<td>2023-02-16 17:25</td>
<td>21</td>
</tr>
</tbody></table>
</br>


<h1 id="마무리하며">마무리하며</h1>
<p>&#39;로드맵 클릭율 감소&#39; 상황을 가정하여 원인을 분석하고 해결 방안으로 실험 가설을 도출해보았다. 최근에 &lt;데이터 문해력&gt; 책을 다시 읽으면서 배운 내용들을 적용하려 노력해봤으나.. 문제 정의부터 원인을 분석하는 과정에서 여전히 설득력이 부족한 것 같다. 서비스의 유저 분석 사례를 더 찾아보며 공부해야겠다.</p>
</br>

<h1 id="참고한-서적">참고한 서적</h1>
<ul>
<li>카시와기 요시키 지음 / &lt;데이터 문해력&gt; / 강모희 옮김</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[SQL, LAG 함수의 쓸모]]></title>
            <link>https://velog.io/@hong_journey/SQL-LAG-%ED%95%A8%EC%88%98%EC%9D%98-%EC%93%B8%EB%AA%A8</link>
            <guid>https://velog.io/@hong_journey/SQL-LAG-%ED%95%A8%EC%88%98%EC%9D%98-%EC%93%B8%EB%AA%A8</guid>
            <pubDate>Sun, 12 Mar 2023 12:13:33 GMT</pubDate>
            <description><![CDATA[<p>LAG와 LEAD는 주로 시계열 데이터를 분석할 때 많이 쓴다고 배웠다. 그런데 정작 쿼리 연습할 때 이 함수들을 사용해본 경우가 드물었는데, 사용 예시를 정리해두면 나중에 적용하기 편하겠다는 생각이 들었다. 함수 정의와 예시는 <a href="https://cloud.google.com/bigquery/docs/reference/standard-sql/navigation_functions?hl=ko#lag">BigQuery</a>를 기준으로 작성했다. </p>
</br>
</br>

<h1 id="1-lag-함수-정의">1. LAG 함수 정의</h1>
<pre><code class="language-sql">LAG(expression [,offset][, default_value]) OVER over_clause</code></pre>
<blockquote>
<ul>
<li>expression : 칼럼명</li>
</ul>
</blockquote>
<ul>
<li>offset : 기본값은 1. window frame 상에서 몇 번째 이전 행의 값을 반환할지 결정.  </li>
<li>default_value : 이전 행이 존재하지 않을 때 대체할 값. 기본값으 NULL</li>
<li>over_clause : PARTITION BY나 ORDER BY 를 Optional하게 사용 가능</li>
</ul>
<p>over_clause와 window frame 구문에 대한 자세한 설명은 <a href="https://velog.io/@hong_journey/SQL-%EC%95%8C%EB%8B%A4%EA%B0%80%EB%8F%84-%EB%AA%A8%EB%A5%B4%EA%B2%A0%EB%8A%94-WINDOW-%ED%95%A8%EC%88%98%EC%99%80-%EC%98%88%EC%8B%9C%EB%93%A4#:~:text=%EB%8C%80%EC%97%AC%20%EC%8B%9C%EA%B0%84%20%EC%B0%BE%EA%B8%B0.-,1.%20Syntax,-Window%20%ED%95%A8%EC%88%98%EB%8A%94%20%EC%9C%88%EB%8F%84%EC%9A%B0%EB%9D%BC%EA%B3%A0">이곳에</a></p>
</br>
</br>



<h1 id="2-목적">2. 목적</h1>
<p><strong>사용한 데이터</strong></p>
<p>BigQuery의 공개 데이터 중 &quot;san_francisco&quot; Dataset에서 &quot;bikeshare_trips&quot; Table을 사용했다. 2015-01-01부터 2016-08-31까지 샌프란시스코 지역의 자전거 대여/반납 내역이 기록된 테이블이다. </p>
<ul>
<li><p>스키마
<img src="https://velog.velcdn.com/images/hong_journey/post/a30884cd-16f4-4000-93a7-a55850341e5a/image.png" alt=""></p>
</li>
<li><p>미리보기
<img src="https://velog.velcdn.com/images/hong_journey/post/c893a6d3-bef2-4b61-a5f6-0bdbf5dcedce/image.png" alt=""></p>
</li>
</ul>
</br>
</br>


<h2 id="목적-1-지난주-이용량과-이번주-이용량-비교하기">목적 1. 지난주 이용량과 이번주 이용량 비교하기</h2>
<p>테이블의 칼럼 값이 정렬된 상태에서 LAG함수로 n번 이전 행에 해당하는 값을 가져올 수 있다. 예를 들어 다음과 같은 분석이 가능하다. </p>
<ul>
<li>자전거 이용량의 요일별 패턴 분석</li>
<li>월별 이용량이 증가한 요인을 분석할 때, 전년 동기에도 비슷한 현상이 있었는지 확인할 때</li>
</ul>
<p>다음은 2015년 자전거 이용량의 요일별 패턴을 분석하고자, 지난주 이용량(<code>previous_week</code>)과 이번주 이용량(<code>num_trips</code>)을 비교한 결과다. </p>
<pre><code class="language-sql">-- (1) 일별 이용량 집계
WITH updates AS(
  SELECT
    DATE(start_date) AS trip_date,
    COUNT(*) AS num_trips
  FROM `bigquery-public-data.san_francisco.bikeshare_trips` 
  WHERE EXTRACT(YEAR FROM start_date) = 2015
  GROUP BY 1
)

-- (2) 이번주와 지난주 이용량 비교
SELECT
  trip_date,
  num_trips,
  LAG(num_trips, 7)
    OVER(
      ORDER BY trip_date
      ) AS previous_week
FROM updates
ORDER BY 1</code></pre>
<ul>
<li>(1) updates 테이블 : 2015년 데이터만 가져와 일별 자전거 이용량을 집계했다. </li>
<li>(2) LAG 함수로 7일 전 이용량을 가져왔다. 7일전 날짜 데이터가 없는 경우 NULL값 처리되었다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/492954c8-be75-4f09-af1a-22644a17aa88/image.png" alt=""></p>
</br>
</br>



<h2 id="목적-2-결측missing-value-확인">목적 2. 결측(Missing Value) 확인</h2>
<p>그런데 앞서 작성한 쿼리에는 문제가 있다. <code>trip_date</code>이 연속되지 않을 수 있다. 즉, 자전거 대여 기록이 <strong>매일</strong> 누락 없이 기록되었으라는 보장은 없다. 데이터를 수집하는 과정에서 얼마든지 결측이 발생했을 수 있다. 이를 확인하기 위해 LAG함수를 사용할 수 있다. </p>
<pre><code class="language-sql">-- (1) 일별 이용량 집계
WITH updates AS(
  SELECT
    DATE(start_date) AS trip_date,
    COUNT(*) AS rows_added
  FROM `bigquery-public-data.san_francisco.bikeshare_trips` 
  WHERE EXTRACT(YEAR FROM start_date) = 2015
  GROUP BY 1
), 
-- (2) 업데이트된 기록 간격 구하기
num_days_update AS(
  SELECT
    trip_date,
    DATE_DIFF(
      trip_date,
      LAG(trip_date, 1)
        OVER(
          ORDER BY trip_date
          ),
      DAY
    ) AS days_since_last_update
  FROM updates
)

-- (3) 결측 날짜 확인
SELECT * FROM num_days_update WHERE days_since_last_update &gt; 1</code></pre>
<ul>
<li>(1) updates 테이블 : 일별 이용량 집계</li>
<li>(2) num_days_update : 업데이트된 기록 간격 구하기. LAG함수로 이전 행과 현재 행의 날짜 간격 계산함. </li>
<li>(3) <code>days_since_last_update</code> 가 n일 초과인 경우 확인. 임계값 기준 n은 정하기 나름. 예시에선 1일 초과된 경우를 확인했다.  </li>
</ul>
<p>쿼리 결과는 다음과 같다. </p>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/7cbe22dc-8e13-4274-9c5f-f3d795fe777e/image.png" alt="">
2015년 일별 이용량 데이터는 <code>days_since_last_update</code>  칼럼 값이 모두 1이다. (모두 1일 간격으로 업데이트 되었다.) </p>
<p><code>LAG(num_trips, 7))</code> 값이 7일 전의 값이 되려면 &quot;num_trips가 하루도 누락없이 매일 기록되었다&quot; 라는 전제가 있어야 한다.  </p>
<p>매일 기록되어야 했을 자전거 대여 기록에서 누락이 발견된다면? 이상 징후로 Alert하고, 데이터 계보 그래프를 확인해야한다. 만약 선행 테이블이 있다면 선행 테이블의 기록 과정에서 발생한 문제인지 확인이 필요하다.</p>
</br>
</br>



<h1 id="3-예시">3. 예시</h1>
<p>2015년 샌프란시스코의 자전거 대여량 추세를 확인한다고 하자. 이를 위해 <code>monthly_average_num_trips</code>을 계산해 시각화하려 한다.</p>
<ul>
<li><code>monthly_average_num_trips</code> : 최근 30일간의 평균 이용량</li>
</ul>
<p><strong>1단계 : 일별 이용량 집계</strong></p>
<pre><code class="language-sql">WITH updates AS(
  SELECT
    DATE(start_date) AS trip_date,
    COUNT(*) num_trips
  FROM `bigquery-public-data.san_francisco.bikeshare_trips` 
  WHERE EXTRACT(YEAR FROM start_date)=2015
  GROUP BY 1
)</code></pre>
</br>

<p><strong>2단계 : 일별 이용량에 누락된 날짜가 있는지 확인</strong></p>
<pre><code class="language-sql">WITH num_days_update AS(
  SELECT
    trip_date,
    DATE_DIFF(
      trip_date,
      LAG(trip_date, 1)
        OVER(
          ORDER BY trip_date
          ),
      DAY
    ) AS days_since_last_update
  FROM updates
)
-- 마지막으로 업데이트 된 기록이 
SELECT * FROM num_days_update WHERE days_since_last_update &gt; 1</code></pre>
<ul>
<li>기록이 누락된 날짜가 없음을 확인</li>
</ul>
</br>

<p><strong>3단계 : 최근 30일간 평균 이용량 구하기</strong></p>
<ul>
<li>monthly_average_num_trips : x일 기준으로 최근 30일간 평균 이용량 계산</li>
<li>next_num_trips : LEAD함수로 (x+1) 일에 해당하는 이용량 가져옴<pre><code class="language-sql">SELECT
trip_date,
AVG(num_trips)
  OVER(
    ORDER BY trip_date
    ROWS BETWEEN 29 PRECEDING AND
    CURRENT ROW
  ) AS monthly_average_num_trips,
LEAD(num_trips, 1)
  OVER(
    ORDER BY trip_date
  ) AS next_num_trips
FROM updates
ORDER BY 1</code></pre>
</li>
</ul>
<p>*<em>4단계 : 시각화 *</em>
<img src="https://velog.velcdn.com/images/hong_journey/post/f7ebac41-07dd-4122-bcf9-20b1c090b906/image.png" alt=""></p>
<p>쿼리를 실행하고 나면 쿼리 결과 테이블을 바로 Google Colab으로 가져올 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/44f62425-3183-422a-bcf7-864ce3293e43/image.png" alt=""></p>
<p>monthly_average_num_trips의 추이를 시각화 한 결과, 2015년 11월 이후로 자전거 이용량이 감소했음을 파악할 수 있다. </p>
<p>최근 30일간 평균 이용량(monthly_average_num_trips, 파랑색)으로 다음날 이용량(next_num_trips, 주황색)을 예측할 수 있을까? 자전거 이용량은 요일별 패턴이 강하다보니 monthly_average_num_trips는 예측 목적으로는 적절하지 않아보인다. </p>
<h1 id="4-참고한-자료">4. 참고한 자료</h1>
<ul>
<li><a href="https://cloud.google.com/bigquery/docs/reference/standard-sql/navigation_functions?hl=ko#lag">BigQuery 공식 문서</a></li>
<li><a href="https://www.oreilly.com/library/view/data-quality-fundamentals/9781098112035/">Data Quality Fundamentals</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[퍼널 정의하고 로그 (다시) 설계하기]]></title>
            <link>https://velog.io/@hong_journey/%ED%8D%BC%EB%84%90-%EC%A0%95%EC%9D%98%ED%95%98%EA%B3%A0-%EB%A1%9C%EA%B7%B8-%EB%8B%A4%EC%8B%9C-%EC%84%A4%EA%B3%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hong_journey/%ED%8D%BC%EB%84%90-%EC%A0%95%EC%9D%98%ED%95%98%EA%B3%A0-%EB%A1%9C%EA%B7%B8-%EB%8B%A4%EC%8B%9C-%EC%84%A4%EA%B3%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 26 Feb 2023 14:53:30 GMT</pubDate>
            <description><![CDATA[<h1 id="1-프로젝트-소개">1. 프로젝트 소개</h1>
<p>팀원들과 딥러닝 모델을 활용한 이미지 복원 서비스를 개발했던 적이 있습니다. 서비스 이용자가 이미지 편집 도중 느끼는 어려움을 파악하기 위해 설문조사를 실시하고, 이를 기반으로 &#39;이미지에서 물체 지우기&#39;, &#39;화질 개선하기&#39;, &#39;흔들림 제거하기&#39; 3가지 작업을 수행할 수 있는 서비스를 기획했습니다. (데모 영상을 포함한 깃헙 링크는 <a href="https://github.com/hongjourney/final-project-level3-cv-14">이곳</a>에 있습니다.)</p>
<p>제가 맡은 역할 중 하나는 유저 로그를 정의하고 데이터베이스를 구축하는 일이었습니다. 데이터베이스를 구축한 목적은 다음과 같습니다. </p>
<ul>
<li>딥러닝 모델을 활용한 서비스라, 배포 이후에 모델 성능을 개선하는 배치 서빙을 계획했습니다. (가능하다면) 사용자의 입력 데이터와 모델이 추론한 결과를 저장해, 추가 학습을 위한 데이터로 활용하고자 했습니다. </li>
<li>딥러닝 모델을 사용하려면 서버 자원이 필요했는데, 실제 배치 서빙을 할 수 있을 만큼 서버 자원을 빌릴 수 있는 기간이 길지 않았습니다. 그래도 배치 서빙을 할 수 있다고 가정하고, 모델 성능 개선이 목적인 데이터베이스를 구축했습니다.</li>
</ul>
</br>
</br>

<h1 id="2-전체적인-흐름">2. 전체적인 흐름</h1>
<p>&#39;이미지에서 물체 지우기&#39; 작업의 예시입니다.
<img src="https://velog.velcdn.com/images/hong_journey/post/5c9d194a-d502-4e1b-9bb6-3ebeeec0ed53/image.png" alt=""></p>
<blockquote>
<p>1) 사용자가 이미지를 업로드하면
2) Tool을 선택하고 (직사각형 그리기, 자유롭게 그리기 등)
3) 원하는 Inference 종류를 선택한다 (이미지에서 물체 지우기, 화질 개선하기, 흔들림 제거하기 등)
4) Inference 결과가 나오면 다운로드 버튼을 눌러 저장한다.
5) 결과에 대해 평점을 매긴다.</p>
</blockquote>
</br>
</br>

<p><img src="https://velog.velcdn.com/images/hong_journey/post/b008c674-3a68-44fe-8991-ac97e0c7c2f7/image.png" alt=""></p>
<blockquote>
<ul>
<li>앱서버 : Streamlit Cloud</li>
<li>데이터베이스 서버 : GCP의 MySQL 인스턴스</li>
<li>모델 학습용 이미지 저장 스토리지 : GCP의 Google Cloud Storage</li>
</ul>
</blockquote>
<p>필요에 따라 서버를 각각 스케일업 할 수 있도록 앱서버(Streamlit Cloud Server), 데이터베이스 서버(Cloud SQL), 스토리지(GCS)로 분리하여 설계했습니다.  </p>
<ul>
<li>사용자가 이미지를 업로드하고, 원하는 Tool과 Inference를 선택하면</li>
<li>Streamlit Cloud Server에서 이미지 데이터를 모델에 입력해 추론 결과 이미지가 생성됩니다. </li>
<li>추론이 완료될 때마다, Google Cloud Storage에 입력 이미지와 결과 이미지로 구성된 Pair Dataset이 저장됩니다. </li>
<li>Storage에 이미지 파일이 업로드되면, 이미지 url이 생성됩니다. 입력 이미지 url과 결과 이미지 url을 앱서버에 보내고, inference 작업의 종류(Inference Type)을 함께 데이터베이스 서버에 보내 테이블에 기록했습니다.<ul>
<li>상단 우측 그림과 같이 총 4개의 테이블을 정의했습니다.<ul>
<li><code>Input</code> : 입력 ID, 입력 이미지 url, 업로드 시간</li>
<li><code>Inference</code> : ID, 입력 ID, 결과 이미지 url, 작업 종류(Inference Type), 결과 이미지가 생성된 시간</li>
<li><code>Mask</code> : 유저가 이미지의 어떤 영역을 지우려고 했는지 기록. 생성 모델의 입력으로 필요.</li>
<li><code>Score</code> : 추론 결과에 대한 유저의 평가점수를 함께 기록.</li>
</ul>
</li>
<li>추후에 이미지 자체를 집계하거나 조회할 필요가 없었으므로 데이터베이스에 저장하지 않고 Storage에 따로 저장했습니다. (이미지를 Byte String으로 변환하면 테이블에도 저장 가능하긴 합니다.)</li>
</ul>
</li>
<li>모델을 재학습하는 시점에는 Target Date 기간에 해당하는 데이터를 불러와 배치서빙할 수 있도록 SELECT 문을 작성했습니다. </li>
</ul>
</br>
</br>

<h1 id="3-문제점-혹은-고민-지점">3. 문제점 혹은 고민 지점</h1>
<p>Streamlit 프레임워크를 사용해 빠르게 프로토타입을 만든 후, 직접 테스트하다 보니 몇 가지 문제점을 발견했습니다. 마우스로 일일이 물체를 마스킹하다 보니 시간이 오래 걸렸고, 유저가 물체의 경계를 타이트하게 지울 경우, 모델 추론 결과가 얼룩덜룩한 문제가 있었습니다.
<img src="https://velog.velcdn.com/images/hong_journey/post/82b5d6ee-4da9-4e1a-a0f2-1e6ac3ececaa/image.png" alt=""></p>
<h3 id="개선--segmentation-tool-추가">개선 : Segmentation Tool 추가</h3>
<p>이 문제를 개선하기 위해, 마우스로 직접 마스킹하는 대신 유저가 지울법한 영역을 Segmentation 모델이 대신 추천해주는 기능을 추가하기로 했습니다. 아래처럼 Segmentation Tool을 사용하면 유저가 이미지에서 지울법한 영역을 제안해주고, 유저는 원하는 영역을 고르기만 하면 되는 식이었습니다.
<img src="https://velog.velcdn.com/images/hong_journey/post/88b43f21-4bcf-46e5-bd8d-2947ed78cde2/image.png" alt=""></p>
<p>그런데 프로젝트를 회고하다 보니, <strong>실제로 얼만큼의 개선이 있었는지 확인할 지표가 없었다는 것을 알게 되었습니다</strong>. 데이터베이스를 설계한 목적이 &#39;모델의 추가 학습&#39;이었기 때문에, <strong>기능을 추가했을 때 서비스가 개선되었는지 확인할 수 있는 지표, 그리고 지표를 계산하기 위한 유저 로그가 부족하다</strong>는 생각이 들었습니다. </p>
<p>프로젝트 이후 프로덕트 분석 기법을 공부하다 &#39;퍼널&#39;이라는 개념을 새로 알게 되었고, 퍼널 개념을 적용하여 유저 로그를 새로 정의해보기로 했습니다. </p>
</br>
</br>

<h1 id="4-퍼널-설계">4. 퍼널 설계</h1>
<p>퍼널 분석이란, 유저가 서비스 최초 유입부터 최종 목적지까지 미리 설계한 흐름대로 잘 도착하고 있는지 분석하는 기법입니다. &#39;이미지에서 물체 지우기&#39; 작업의 퍼널은 다음과 같이 정의했습니다.</p>
<blockquote>
</blockquote>
<ol>
<li>이미지 업로드</li>
<li>Tool 선택</li>
<li>Inference 선택</li>
<li>이미지 저장</li>
<li>스코어 입력</li>
</ol>
<p>퍼널 설계를 바탕으로 테이블을 다시 정의해보았습니다. </p>
</br>
</br>

<h1 id="5-테이블-재정의">5. 테이블 재정의</h1>
<p>중복을 피하기 위해 Fact Table과 Dimension Table로 구분해 정의했습니다. Fact Table은 집계에 기반이 되는 테이블이며 시간이 함께 기록됩니다 (Table 1~ Table 4). Dimension Table은 주로 데이터를 분류하기 위한 속성값이 기록되고 상황에 따라 수정될 수 있기 때문에 따로 분리했습니다 (Table 5, Table 6).</p>
<p><strong>Table 1) Input 이미지 : 유저가 이미지 업로드 시 INSERT</strong></p>
<ul>
<li><code>input_id</code> : 입력 ID (입력한 이미지에 해당하는 UUID)</li>
<li><code>input_url</code> : 입력 이미지 url</li>
<li><code>server_time</code> : 업로드 시각</li>
</ul>
<p><strong>Table 2) Tool 클릭 : 유저가 Tool 클릭 시 INSERT</strong></p>
<ul>
<li><code>id</code> </li>
<li><code>input_id</code> : 입력 ID</li>
<li><code>tool_id</code> : Tool ID</li>
<li><code>server_time</code> : Tool 클릭 시각</li>
</ul>
<p><strong>Table 3) Inference 클릭 : 유저가 Inference 클릭 시 INSERT</strong></p>
<ul>
<li><code>id</code></li>
<li><code>input_id</code> : 입력 ID</li>
<li><code>inference_id</code> : 추론 작업 ID</li>
<li><code>inference_url</code> : 추론 이미지 url</li>
<li><code>mask_url</code> : (모델 학습에 필요한) mask 이미지 url</li>
<li><code>server_time</code> : Inference 클릭 시각</li>
</ul>
<p><strong>Table 4) Save 클릭 : 유저가 Save 클릭 시 INSERT</strong></p>
<ul>
<li><code>id</code> </li>
<li><code>input_id</code> : 입력 ID</li>
<li><code>server_time</code> : Save 클릭 시각</li>
</ul>
<p><strong>Table 5) Tool 분류 : 마스터 테이블</strong></p>
<ul>
<li><code>tool_id</code> : Tool ID (int)</li>
<li><code>tool_name</code> : Tool 이름 (string)</li>
<li><code>tool_type</code> : Tool 카테고리 (string)</li>
</ul>
<p><strong>Table 6) Inference 분류 : 마스터 테이블</strong></p>
<ul>
<li><code>Inference_id</code> : Inference ID (int)</li>
<li><code>Inference_name</code> : Inference 이름 (string)</li>
<li><code>Inference_type</code> : Inference 카테고리 (string)</li>
</ul>
</br> 
</br>

<h1 id="6-분석을-위한-테이블-만들기">6. 분석을 위한 테이블 만들기</h1>
<p>데이터를 기록하는 단계에선 위와 같이 Fact Table과 Dimension Table로 분리해두고, 분석하는 단계에서는 분석에 필요한 테이블들을 결합해 비정규화 테이블을 만듭니다. </p>
<p>Segmentation Tool을 추가했을 때, 유저의 작업 시간이 실제로 감소하는지 확인하기 위해 분석을 수행하고자 합니다. Tool 클릭 테이블, Inference 클릭 테이블, Save 클릭 테이블을 결합해 다음과 같은 비정규화 테이블을 생성해보겠습니다.  </p>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/bf5614a6-a969-484c-8efe-9ed04a319368/image.png" alt=""></p>
<p>&lt;예시1&gt;은 입력 이미지마다 Tool을 처음으로 클릭하고, 모델에 입력하고, 추론된 결과 이미지를 저장하기까지의 시간 기록을 가져온 테이블입니다. 이 때 Input ID별로 &quot;제일 처음 Tool을 클릭한 시간&quot;과 &quot;이미지를 저장한 시간&quot;을 구하고, Input ID별로 두 시간의 차이(=총 작업 시간 &quot;Diff Second&quot;)을 초단위로 집계할 수 있습니다. &lt;예시2&gt;
<img src="https://velog.velcdn.com/images/hong_journey/post/d52fe022-17b2-4af7-b3d6-64871d0ef5c9/image.png" alt=""></p>
<p><u>기존 서비스</u>와 <u>Segmentation Tool을 새로 추가한 서비스</u>의 Diff Second의 평균을 비교해 유저의 작업 시간이 실제로 감소했는지, 감소했다면 어느정도 감소했는지를 집계할 수 있습니다. </p>
</br> 
</br>


<h1 id="7-정리하며">7. 정리하며</h1>
<p>과거에 진행했던 프로젝트에서 &#39;기능을 추가하여 문제를 개선했다&#39;라고만 정리하고, 지표로 확인하기 위한 로그 정의를 제대로 하지 않았다는 것을 발견했습니다. 퍼널을 기준으로 로그를 다시 정의했고, 팩트 테이블과 디멘전 테이블로 구분해 테이블을 재정의했습니다. </p>
<p>클릭 이벤트 위주로 아주 간단히 로그를 정의해보았는데, 더 다양한 이벤트 파라미터를 고려해볼 수 있을 것 같습니다. AB테스트를 고려한다면 한 명의 유저가 대조군과 실험군에 동시에 들어가면 안 되기 때문에 유저 로그를 더 상세히 정의해야할 것 같습니다. 이밖에 데이터 QA를 자동화하는 방법에 대해서도 더 공부해야할 것 같습니다...! </p>
</br> 
</br>


<h1 id="8-참고한-자료">8. 참고한 자료</h1>
<ol>
<li>빅데이터를 지탱하는 기술 / 니시다 케이스케 지음 / 정인식 옮김 / 제이펍</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[글또 8기를 시작하며]]></title>
            <link>https://velog.io/@hong_journey/%EA%B8%80%EB%98%90-8%EA%B8%B0%EB%A5%BC-%EC%8B%9C%EC%9E%91%ED%95%98%EB%A9%B0</link>
            <guid>https://velog.io/@hong_journey/%EA%B8%80%EB%98%90-8%EA%B8%B0%EB%A5%BC-%EC%8B%9C%EC%9E%91%ED%95%98%EB%A9%B0</guid>
            <pubDate>Sun, 12 Feb 2023 10:58:17 GMT</pubDate>
            <description><![CDATA[<p>글또 8기를 시작하며 작년에 작성했던 글들을 읽어보았다. 주로 공부 기록 목적의 글을 작성했고, 그러다 보니 이 글을 누가 읽는지에 대한 고민이 부족했다. </p>
<p>그래서 올해에는 <strong>&#39;적용&#39;으로 마무리되는 글을 많이 작성하는 것이 목표이다.</strong> Towards Data Science 같은 글을 읽을 때도, 예시가 포함된 경우가 더 이해가 잘 되었던 것 같다. </p>
<p>예를 들어 새로 알게된 분석 기법에 관한 글을 작성한다면, </p>
<ul>
<li>활용 가능한 예시를 가정해 데이터를 수집하고</li>
<li>적용해서</li>
<li>&#39;어떤 문제를 해결할 수 있는지&#39;에 대한 글을 작성해보고 싶다. </li>
</ul>
<p>지난 글또에서 작성했던 글 중, 적용으로 마무리했던 글은 한 개뿐이다. 이번에는 이런 글을 최소한 3개 쓰는 것이 목표다. 분량이 많다면 시리즈로 쪼개는 방법도 있을 것 같다.</p>
</br>
</br>



<h1 id="데이터-프로덕트를-만들자">데이터 프로덕트를 만들자</h1>
<p>최근에 봤던 면접에서 &quot;데이터 분석가는 추출이나 집계하는 요청 업무를 주로 하게 될 수도 있다, 그래도 상관없는지?&quot;라는 질문을 받았는데 면접 복기를 하다가 뜨끔했다. 나는 (빨리 일을 시작해 경험을 쌓고 싶다는 마음이 앞서서) &quot;오히려 재밌을 것 같다. 나는 호기심이 많은 편이라, 오히려 추출과 집계 업무를 하며 서비스를 잘 이해할 수 있을 것 같다&quot;라는 답변을 했다. </p>
<p>그런데 질문의 의도는 &#39;요청 업무가 잘 맞냐 or 맞지 않냐&#39;를 물어본 것이 아니었을 수도 있겠다는 생각이 들었다. 실제로 분석가로 일하시는 분들을 만났을 때 ‘팀에 조인했을 때, 요청 형태로 일을 하다 보니 쿼리와 시각화 머신이 된 듯하다’는 이야기를 많이 들었다. 즉, 이건 분석가로서 한 번쯤은 고민하게 될 문제다. 그런 문제에 대한 해결 방안 고민 없이, 아뇨! 그건 저에게 문제가 아닌데요!라고 하는 건 설득력 없는 답변이었다.</p>
<p>올해 경기가 좋지 않아 취업 기회가 더욱 좁아졌다고는 하나, 최근에 봤던 면접들을 모두 복기해보았을 때 면접에서 떨어졌던 결정적인 이유는 나에게 있다고 생각한다. </p>
<p>나의 문제는, </p>
<ol>
<li><p><strong>그 팀이 현재 가장 중요하게 생각하는 문제를 파악하지 못했다.</strong> 최근 본 면접에서 &#39;이 팀에 일하면서 어려운 점이 어떤 것이 있는지&#39;에 대해 질문했는데, 답변을 듣고 내가 어떤 역할 또는 시도해 보겠다는 어필을 하지 못하고 나온 것이 아쉬웠다. 이 부분을 보완하려면 면접을 준비하는 과정에서 유사한 분야에서 일하는 분석가를 만나 힌트를 얻을 수도 있겠고, 면접 당시에 면접관에게 &#39;바라는 스킬셋이나 모습이 무엇인지&#39;를 질문해서 유추할 수 있을 것 같다.</p>
</li>
<li><p><strong>나도 나를 몰라서 면접관에게 나를 설명하는 것이 설득력이 없다.</strong> 나는 그동안 면접을 준비하며 회사의 서비스와 업무를 이해하기 위해 정보를 찾고 나를 거기에 잘 맞추려고 노력했다. 물론 그것도 중요하겠지만, 정작 내가 이 일을 왜 하고 싶은지 깊게 질문하지 않았다. 어떤 것을 좋아하고 어떤 것을 잘하는지 뾰족하지가 않다. 나는 5년 후에 어떤 커리어를 쌓아나가길 바라지?? (모름.) 나는 어떤 사람이지?? (당황.) 나는 뭘 좋아하고 뭘 싫어하지?? (잘 기억이 안 남.) </p>
</li>
</ol>
<p>이를 바탕으로 새로 잡은 목표는 데이터 프로덕트를 만드는 연습을 하는 것이다. 나는 쿼리 작성하는 건 자신 있다. 잘하는 건 아니지만 부족한 부분을 발견하면 빠르게 보완 가능하다. 그런데 쿼리 작성만 데이터 분석가의 역할일까? 로그 정의, 집계 방식 결정, 분석 환경 세팅 업무를 하게 될 수도 있다. 요청 업무가 반복될 경우, Summary Table 만드는 방법을 고민해야 할 수도 있다. </p>
<p>(아직 협업해 본 적은 없지만) 기획자, 마케터, 디자이너 등 다양한 사람들과 같이 일한다면, 팀원들의 니즈를 해결하기 위해 데이터 프로덕트를 만들어두고 커뮤니케이션하며 조금씩 확장해나가는 일을 하고 싶다. 데이터 프로덕트를 만들기 위해 데이터 엔지니어링 공부가 필요하다고 생각했다. 일단 빠르게 사이클을 한 바퀴 돌려보는 프로젝트를 해야 할 것 같고, 그 과정을 블로그에 작성해야겠다.</p>
</br>
</br>

<hr>
<h1 id="마무리하며">마무리하며</h1>
<p>작년에 글또를 통해 감사한 기회도 많이 생겼고, 이전보다 작성한 글의 개수도 늘어나서 참여하길 잘했다는 생각이 들었다. 그런데 막상 8기 모집 소식을 듣고 많이 고민했는데, 그 이유는 취준생 입장에서 내가 공유할 수 있는 게 많지 않다는 생각이 들어서이다. 글또 커피챗을 통해 내가 얻은 것은 많지만, 정작 나는 글또를 통해 어떤 기여(?)를 할 수 있을지 구체적으로 그려지지 않았다. </p>
<p>그럼에도 다시 신청한 이유는 &#39;이때에만 할 수 있는 생각이 있고, 이때에만 시도해볼 수 있는 프로젝트가 있을 것이다&#39;라는 생각 때문이다. 고민되지만 일단 해보자, 라고 결론을 낸 데에는 글또 피드백 문화도 있다. 비록 내가 작성한 글은 엉성할지라도 최ㅊ최최최종 글은 분명 나아질 것이라 믿는다. </p>
<p>완주를 향하여~ 🏃‍♀️🏃‍♂️</p>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/797002ed-b787-43fb-9f2b-745ee3b68ac7/image.jpg" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Python으로 Stack 구현하기]]></title>
            <link>https://velog.io/@hong_journey/Python%EC%9C%BC%EB%A1%9C-Stack-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hong_journey/Python%EC%9C%BC%EB%A1%9C-Stack-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 17 Nov 2022 17:52:57 GMT</pubDate>
            <description><![CDATA[<p>Python으로 객체지향 프로그래밍(oop)를 공부하고 있다. 오늘은 간단하게 자료구조 중 하나인 Stack 구현하면서 typing이나 상속 개념을 정리해보겠다. (이 글은 &quot;<a href="https://inf.run/umKZ">타입 파이썬! 올바른 class 사용법과 객체지향 프로그래밍-  인프런</a>&quot; 강의를 참조하여 작성했습니다.)</p>
<h2 id="1-stack">1. Stack</h2>
<p>Stack이란 쌓아 올린 형태의 자료구조를 말한다. Python에서는 연결리스트를 직접 구현할 필요없이 리스트 자료형의 append와 pop 매서드로 Stack을 사용할 수 있다. 박스를 쌓고 꺼내는 것처럼, 스택 자료구조는 가장 마지막에 들어간 데이터가 가장 먼저 나가는 LIFO(Last In First Out) 구조이다. 
<img src="https://velog.velcdn.com/images/hong_journey/post/371e2d10-96ba-4cff-8c1c-b0c4ae44c692/image.png" alt=""></p>
<p>데이터를 모두 꺼내서 더이상 pop할 수 없으면 스택이 <strong>Empty</strong>하다, 라고 표현하고 반대로 꽉 차서 더이상 스택에 push를 할 수 없는 상황은 <strong>stack overflow</strong>가 발생했다고 한다. </p>
<p>박스처럼 생긴 데이터 단위는 노드(Node)라고 한다. 그리고 앞으로 구현할 연결리스트(Linked List)는 노드로 이루어져있다. 
<img src="https://velog.velcdn.com/images/hong_journey/post/5f268a48-31d8-4295-9c97-6143d285f394/image.png" alt=""></p>
<p>노드는 item, pointer 총 2가지의 속성을 가지고 있는데, pointer가 노드와 노드를 &quot;연결&quot;해주는 역할을 한다. 위의 그림을 보면 Node1의 pointer가 Node2를, Node2의 poiner가 Node3를 가리키는 것을 볼 수 있다. Node라는 클래스를 정의하여 이를 구현하면,</p>
<h2 id="2-node-구현">2. Node 구현</h2>
<pre><code class="language-python">from typing import Optional


class Node:
    def __init__(self, item, pointer: Optional[&quot;Node&quot;] = None):
        self.item = item
        self.pointer = pointer


if __name__ == &quot;__main__&quot;:
    # 노드 생성
    node1 = Node(item=11)
    node2 = Node(item=22)
    node3 = Node(item=33)

    # 노드 연결
    node1.pointer = node2
    node2.pointer = node3
</code></pre>
<p><strong>생성자 Constructor (<code>__init__</code> 함수)</strong></p>
<ul>
<li>인스턴스가 생성된 순간 즉시 실행된다. 인스턴스가 생성되는 순간이 메모리에 올라오는 순간.</li>
<li><code>self</code>는 인스턴스가 생성되었을 때, 그 인스턴스를 지칭한다. </li>
<li>item과 pointer 두가지 속성이 있는데<ul>
<li>item은 노드가 담고 있는 데이터를</li>
<li>pointer는 다음 노드를 저장하거나, 아무것도 가리키지 않으면 None을 저장한다.   </li>
<li>pointer의 디폴트 값은 None이다.</li>
<li>pointer의 타입은 <code>Optional(&quot;Node&quot;)</code>. &quot;Node&quot;이거나 None 이거나. </li>
<li><code>&quot;Node&quot;</code> : 클래스를 typing하는 도중, 자기 자신을 타이핑해야한다면 큰 따옴표를 쓴다.</li>
</ul>
</li>
</ul>
<p><strong>노드 생성, 연결</strong></p>
<ul>
<li>총 3개의 인스턴스가 생성됨. (node1, node2, node3) 각각의 인스턴스는 서로 독립적이다. </li>
<li>node1은 node2에 연결되어 있다. 그래서 node1의 pointer에 node2를 저장한다. node3은 맨 마지막 노드이므로 pointer 값은 None이다. </li>
</ul>
<p><strong>typing</strong></p>
<pre><code class="language-python">int_var: int = 88
str_var: str = 88 # 오류 안 남</code></pre>
<ul>
<li>Python에서 typing은 타입 힌트일뿐, 타입 체크를 하진 않는다. </li>
<li>예를 들어, pointer를 <code>Optional(&quot;Node&quot;)</code>을 사용하여 타이핑했는데, 예상하지 않은 타입을 쓰더라도 실행 시 오류가 나지 않는다.  (<code>node1.pointer = 3</code> 를 실행해도 에러가 안 뜬다.)</li>
<li>타입 체크를 하려면<ul>
<li><code>isinstance(88, int)</code> 함수를 써도 되고, mypy나 pyright 라이브러리를 설치해도 타입 체크 가능 </li>
<li>ex. <code>mypy hello.py &amp;&amp; python hello.py</code> <a href="https://mypy.readthedocs.io/en/stable/getting_started.html">(공식문서 참고)</a></li>
</ul>
</li>
</ul>
</br>
</br>

<h2 id="3-연결-리스트-linkedlist-구현">3. 연결 리스트 LinkedList 구현</h2>
<blockquote>
<p>구현 내용</p>
</blockquote>
<ul>
<li>head 속성 : 가장 첫 번째 노드를 head라고 하고, 노드가 없다면 None을 저장.</li>
<li>length : int 타입. 현재 노드(데이터)의 개수 </li>
<li>print 했을 때 &quot;,&quot;로 구분하여 item 출력</li>
</ul>
<pre><code class="language-python">class LinkedList:
    def __init__(self):
        self.head: Optional[Node] = None

    @property
    def length(self) -&gt; int:
        if self.head is None:
            return 0
        cur_node = self.head
        count: int = 1
        while cur_node.pointer is not None:
            cur_node = cur_node.pointer
            count += 1
        return count

    def __str__(self) -&gt; str:
        # read
        result: str = &quot;&quot;
        if self.head is None:
            return result
        cur_node = self.head
        result += f&quot;{cur_node.item}&quot;  
        while cur_node.pointer is not None:
            cur_node = cur_node.pointer
            result += f&quot;, {cur_node.item}&quot;
        return result</code></pre>
<p><strong>생성자 Constructor (<code>__init__</code> 함수)</strong></p>
<ul>
<li>첫 번째 노드인 head를 정의한다. </li>
<li>타입은 &quot;Node&quot;이거나, None 이므로 <code>Optional(&quot;Node&quot;)</code></li>
<li>디폴트값은 None으로 저장</li>
</ul>
<p><strong>length</strong></p>
<ul>
<li>매서드로 구현해도 되고 <code>@property</code> 데코레이터를 붙여서 속성으로 구현해도 됨. <ul>
<li><code>@property</code> : 접근(read)은 가능하지만, 업데이트는 불가. (캡슐화)</li>
<li><code>+=1</code> 같은 업데이트하면 set 속성이 없다고 에러남(AttributeError: can&#39;t set attribute)</li>
<li>만약 업데이트하려면, <code>@length.setter</code> 데코레이터로 setter 함수 추가해줌.</li>
<li>pop, push할 때마다 length를 업데이트하는 방법도 있겠지만 예상하지 못한 상황에서 length가 임의로 업데이트될 가능성이 있으니, length는 접근만 가능하게 하는 것이 나아보임. </li>
</ul>
</li>
<li>첫번째 노드인 <code>self.head</code> 가 없다면 0 출력</li>
<li>첫번째 노드가 있다면 pointer가 None일 때까지 노드를 카운트</li>
</ul>
<p><strong><code>__str__</code></strong></p>
<ul>
<li>Python의 모든 클래스는 Object 클래스의 상속을 받음. (<code>class LinkedList:</code>  == <code>class LinkedList(Object):</code>)</li>
<li>이 때 Object 클래스에 이미 내장된 매서드들을 Magic Method라고 함. (dir()로 확인 가능 ex. <code>__class__</code>, <code>__init__</code>, <code>__str__</code>)</li>
<li>print문은 객체를 문자열화하는 함수. print를 쓸 때 <code>__str__</code> 이 실행됨. </li>
<li><code>__str__</code> 함수를 새로 정의하여 덮어쓰기(Method Overriding) 가능. </li>
<li>length와 마찬가지로 첫번째 노드가 있는지 확인하고, 있다면 pointer가 None일 때까지 while 문 돌리는 식으로 구현</li>
</ul>
</br>
</br>

<h2 id="4-stack-구현">4. Stack 구현</h2>
<blockquote>
<p>구현 내용</p>
</blockquote>
<ul>
<li>LinkedList를 상속받는다. </li>
<li>push(item) : Stack 자료구조에 item을 받아 Node로 만든 다음 Stack 에 넣는다. </li>
<li>pop() : Stack 자료구조에서 마지막 Node를 제거하고, 해당 item을 반환한다. </li>
</ul>
<pre><code class="language-python">
class Stack(LinkedList):
    def push(self, item) -&gt; None:
        new_node: Node = Node(item=item)
        if self.head is None:
            # head가 가리키는 게 아무것도 없을 때
            self.head = new_node
            return  
        cur_node = self.head
        while cur_node.pointer is not None:
            cur_node = cur_node.pointer
        cur_node.pointer = new_node

    def pop(self):
        if self.head is None:
            raise ValueError(&quot;stack is empty&quot;)
        else:
            cur_node = self.head
        if cur_node.pointer is None:
            self.head = None
            return cur_node.item
        while cur_node.pointer.pointer is not None:  # pointer의 pointer
            cur_node = cur_node.pointer
        result = cur_node.pointer
        cur_node.pointer = None
        return result.item
</code></pre>
<p><strong><code>push(item)</code></strong></p>
<ul>
<li>item 인자가 필요하고 반환하는 값은 없다. (None으로 typing)</li>
<li>연결리스트의 가장 마지막 노드에 해당하는 pointer에 new_node를 추가한다.  </li>
<li>head가 가리키는 게 없다면, 첫번째 노드인 <code>self.head</code>에 new_node를 정의한다. </li>
<li>head가 가리키는 노드가 있다면, 가장 마지막 노드를 찾을 때까지 while문으로 탐색한다. </li>
<li>마지막 노드를 찾았다면 cur_node.pointer에 new_node를 연결한다. </li>
</ul>
<p><strong><code>pop()</code></strong></p>
<ul>
<li>인자가 필요 없고 item 값을 반환한다. </li>
<li>연결리스트의 가장 마지막 노드를 꺼내곰, 직전 노드의 pointer에는 None을 저장한다. </li>
<li>head가 가리키는 게 없다면, Stack이 비어있는 상황이므로 ValueError를 일으킨다.</li>
<li>head가 가리키는 게 있다면, 가장 마지막 노드를 찾을 때까지 while문으로 탐색한다. </li>
<li>cur_node.pointer.pointer가 None인 마지막 노드를 찾아 item을 반환하고, 그 직전 노드인 cur_node.pointer의 pointer는 None으로 저장한다. </li>
</ul>
<p></br></br></p>
<h2 id="5-generic-type">5. Generic Type</h2>
<p>위에서 item의 타입은 int, str, bool 등 여러가지가 가능하다. item의 타입은 자유롭되, 한 스택 안에 포함된 노드들은 모두 동일한 타입을 가지도록 구현할 순 없을까? 예를 들어 int와 str이 섞여있는 (1, 2, &#39;3&#39;, 4) 와 같은 형태는 허용하지 않는 것이다. 이 때, Generic Type을 사용할 수 있다. <strong>Generic Programming</strong>은 데이터 형식에 의존하지 않고, 일반성을 유지하며 하나의 값이 여러 다른 데이터 타입들을 가질 수 있는 기술이다. </p>
<p>Generic Type은 위의 예시처럼 item의 타입을 고정할 때도 쓸 수 있고, item과 동일한 타입인 또 다른 변수를 정의할 때도 쓸 수 있다. typing과 마찬가지로 타입 체크가 되진 않지만 코드로 협업할 때 타입 힌트로 유용하다는 장점이 있다.  </p>
<pre><code class="language-python">from typing import Generic, TypeVar

T = TypeVar(&quot;T&quot;) # Generic Type</code></pre>
<p>위처럼 TypeVar로 타입 변수 T를 만든다. <code>TypeVar(&quot;T&quot;, int, str)</code>로 타입 후보를 제한해줄 수도 있다. (mypy 0.982 버전에서는 오류가 나서 <code>TypeVar(&quot;T&quot;)</code>로 수정 했다.) </p>
<p>Node, LinkedList, Stack 클래스에 각각 item을 T로 타이핑한 코드는 다음과 같다. Node의 item을 LinkedList와 Stack에서도 쓰기 때문에 클래스를 정의할 때 아래와 같이 <code>Generic[T]</code>를 넣어줘야한다.
</br></p>
<h2 id="전체코드">전체코드</h2>
<pre><code class="language-python">from typing import Optional, TypeVar, Generic

T = TypeVar(&quot;T&quot;)


class Node(Generic[T]):
    def __init__(self, item: T, pointer: Optional[&quot;Node&quot;] = None):
        self.item = item
        self.pointer = pointer


class LinkedList(Generic[T]):
    def __init__(self):
        self.head: Optional[Node[T]] = None

    @property
    def length(self) -&gt; int:
        if self.head is None:
            return 0
        cur_node = self.head
        count: int = 1
        while cur_node.pointer is not None:
            cur_node = cur_node.pointer
            count += 1
        return count

    def __str__(self) -&gt; str:
        result: str = &quot;&quot;
        if self.head is None:
            return result
        cur_node = self.head
        result += f&quot;{cur_node.item}&quot;
        while cur_node.pointer is not None:
            cur_node = cur_node.pointer
            result += f&quot;, {cur_node.item}&quot;
        return result


class Stack(Generic[T], LinkedList[T]):
    def push(self, item: T) -&gt; None:
        new_node: Node[T] = Node[T](item=item)
        if self.head is None:
            self.head = new_node
            return  
        cur_node = self.head
        while cur_node.pointer is not None:
            cur_node = cur_node.pointer
        cur_node.pointer = new_node

    def pop(self) -&gt; T:
        if self.head is None:
            raise ValueError(&quot;stack is empty&quot;)
        else:
            cur_node = self.head
        if cur_node.pointer is None:
            self.head = None
            return cur_node.item
        while cur_node.pointer.pointer is not None:  
            cur_node = cur_node.pointer
        result = cur_node.pointer
        cur_node.pointer = None
        return result.item
</code></pre>
<p>length 매서드를 구현할 때 self.head를 고정하지 않고 업데이트하는 바람에 초반에 애를 먹었다. 타이핑하며 구현하는게 아직 익숙하지 않아서 (특히 Generic) 좀 더 연습해야할 것 같다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[글또 7기 회고글]]></title>
            <link>https://velog.io/@hong_journey/%EA%B8%80%EB%98%90-7%EA%B8%B0-%ED%9A%8C%EA%B3%A0%EA%B8%80</link>
            <guid>https://velog.io/@hong_journey/%EA%B8%80%EB%98%90-7%EA%B8%B0-%ED%9A%8C%EA%B3%A0%EA%B8%80</guid>
            <pubDate>Sun, 16 Oct 2022 13:46:05 GMT</pubDate>
            <description><![CDATA[<p>글또 다짐글을 작성(2022.05)한지 딱 6개월 되었는데, 오늘부로 6개월 뒤는 2023년이라는 사실에 잠시 충격(!)을 받았다. 정신차리고 다짐글에 작성했던 목표들을 돌아보면,</p>
</br>

<h2 id="목표를-이뤘나">목표를 이뤘나?</h2>
<p><strong>1. 논문 구현</strong>
좀 다양한 논문을 구현하고 기록하고 싶었지만 정작 기록은 VAE 하나다. 공부해서 정리하는 일과 달리, 예상 독자를 정하고 독자가 알기 쉽게 정리해서 글을 쓰는 것은 쉽지 않다는 것을 느꼈다. 그래도 VAE 개념을 설명하면서 애매모호했던 개념들이 많이 해소되었다. 뭔가 날잡고 하기보다는 매일 조금씩 꾸준히 작성해봐야겠다. </p>
<p><strong>2. 기술 블로그 공부</strong>
기술 블로그를 읽다가 궁금했던 용어와 내용을 정리해서 글을 작성했다. multiclass AB test에 관한 내용이었는데 학부 실험계획법 시간에 배웠던 내용들을 복습할 수 있어서 좋은 기회였던 것 같다. 당초 계획과 달리 데이터 엔지니어링 분야를 정리하는 글을 작성하지는 못해서 아쉽다. </p>
<p><strong>3. 프로젝트 기록</strong>
5개월동안 서비스를 구현하고 그 과정을 기록해보는 것이 목표였는데.. 하지 못했다.. 무엇을 해야할까??? 가 아직도 가장 고민이다..</p>
</br>
</br>

<h2 id="감사했던-기회">감사했던 기회</h2>
<p>글또 7기를 하길 잘했다는 생각이 들만큼 감사한 순간들이 많았다. 
</br></p>
<p><strong>1. 글또 오프라인 (커피챗, 글또콘)</strong>
슬랙으로만 뵙던 분들과 직접 이야기를 나눌 수 있어 좋았다. 일에 대한 다양한 고민을 옆에서 듣는 것만으로도 소중한 시간이었다. 특히 글또콘을 위해 글또 운영진분들이 굉장히 고생하신 것 같았는데, 이런 자리를 기획해 주시고 진행해 주셔서 감사했다..!
</br>
<strong>2. 면접 기회</strong>
평소 가고 싶었던 기업이 있었는데, 그곳에서 일하는 분이 글또에 있어서(!) 업무에 대해 질문도 할 수 있었고 좋은 면접 기회도 얻었다. 내가 그동안 분석가의 역할을 굉장히 단편적으로 이해하고 있었다는 것을 알게되기도 했다. 비록 면접은 떨어져서 슬펐지만.. 커피챗 하면서 일 이야기를 많이 들려주셔서 너무나 감사했다. 
</br></p>
<p><strong>3. 배움의 연속</strong>
글또의 최대 장점은 관심있던 분야의 다른 사람의 글을 읽을 수 있다는 것이고, 두번째 장점은 마감이다. 마감때문에 일단 쓰게된다. 평소라면 여러 글감중에서 고르고 고르다 결국 몇 개월동안 아무런 글 마무리도 짓지 못했을 것이다. </p>
<p>마감에 쫒겨 쓰는 과정이 괴로운 날도 많았지만 다른 사람이 올린 글에 동기부여를 받아 글을 일단 쓰게 되고, 정리하고 나면 생각도 정리되고 공부도 됐다. 이미 알고 있다고 생각한 주제임에도 글로 정리하다보니 뭐라 설명하기 어려운 애매한 용어들을 수정할 수 있었다. 
<img src="https://velog.velcdn.com/images/hong_journey/post/87689dad-cd3f-4259-ba1c-22a24c9ba466/image.png" alt="">
덧붙여 취준 기간에 번아웃되기 쉬운데, 글또는 2주마다 마감이 있었던 덕분에(?) 그럴 틈이 없었던 것 같다. 이걸 글로 내놓아도 되나.. 싶은 정도의 퀄리티 글인데도 피드백으로 공부할 키워드를 알려주셔서 감사했음...
</br></p>
<p><strong>4. 글또 이전과 달라진 점?</strong>
글또 이전에는 내 글을 누가 읽는다는 상상을 해본적이 없고, 그저 예상 독자를 미래의 나로 가정하여 공부한 내용을 까먹지 않기 위해 글을 작성했다. 그런데 글에 대한 피드백을 받고 다른 분들이 쓴 글을 읽으면서, 내 방식을 바꿔봐야겠다는 생각이 들었다. </p>
<p>그럼 어떻게 글을 작성해야하나? 에 대한 질문에 대해 고민하고 있을 때, 이런 <a href="https://euncho.medium.com/%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-%EC%84%B1%EC%9E%A5-%EA%B0%80%EB%8A%A5%EC%84%B1%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%B8%A1%EC%A0%95-%EA%B0%80%EB%8A%A5%ED%95%9C%EA%B0%80-238dd3f0f33?source=email-773903401600-1659116780219-digest.reader--238dd3f0f33----0-2------------------5f5dbf4f_12a2_40a8_9a5e_5c5360cf1818-31" target="_blank">미디움 글</a> 을 발견했다. &lt;개발자의 성장 가능성은 어떻게 측정 가능한가?&gt; 라는 제목의 글인데, 이 글에 따르면 A라는 기능을 이해하는 과정은 이렇다. </p>
<ul>
<li>기존에 어떻게 동작했는가?</li>
<li>A는 어떤 부분을 해결하였는가?</li>
<li>A는 어떻게 동작하는가?</li>
<li>기존 코드에서 어떻게 A를 사용한 코드로 바꿀 수 있는가?</li>
</ul>
<p>글이 안 써져서 머리를 쥐어 뜯는 와중에 도움을 받은 글이었다. </p>
</br>
</br>

<h2 id="마무리">마무리</h2>
<p>글또를 안 했다면 어땠을까? 싶은 생각이 들만큼 5개월동안 내게 큰 변화가 있었던 것 같다. 그 말은 즉.. 글또가 아니었다면 아마 내 블로그는 텅텅 비어있지 않았을까..? 다음 기수에도 지원할 수 있다면 꼭 참여하고 싶다!   </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AB 테스트 설계 용어 (2) / Multiclass AB test 설계]]></title>
            <link>https://velog.io/@hong_journey/AB-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%84%A4%EA%B3%84-%EC%9A%A9%EC%96%B4-2-Multiclass-AB-test-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@hong_journey/AB-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%84%A4%EA%B3%84-%EC%9A%A9%EC%96%B4-2-Multiclass-AB-test-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Sun, 16 Oct 2022 11:34:34 GMT</pubDate>
            <description><![CDATA[<p>기술블로그에서 서비스를 개선하거나, 새로운 기능을 추가하여 배포할 때 <strong>AB 테스트</strong> 기반으로 의사결정한다는 말을 자주 접한다. 구체적으로 어떤 상황에서 많이 쓸까?</p>
<blockquote>
<p><strong>언제 AB 테스트를 쓸까?</strong></p>
</blockquote>
<ul>
<li>유저의 클릭율이 높은 썸네일이 뭘까? (ex. 유튜브, 넷플릭스)</li>
<li>A 기능의 UI를 개선해볼까? <ul>
<li>전환율을 높이려면 총 4가지 UI 후보 중 어떤 후보를 선택할까?</li>
<li>전환율이 0.3% 개선되었다면 이게 얼마나 영향이 있는 개선인가? </li>
</ul>
</li>
<li>B 기능을 추가하면 유저의 구매전환율이 높아질 것 같은데, 실제로 그럴까 or 예상치 못한 불편함을 주진 않을까</li>
</ul>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/137cd883-5c7b-48ad-8216-190ae4636031/image.png" alt=""></p>
<p>위 그림은 <a href="https://medium.com/daangn/직관만-믿고-까불었다가-망한-pm의-사연-61946dc969eb" target="_blank"> 당근마켓 블로그</a> 에서 소개한 키워드 알림 버튼의 위치를 변경했던 예시다. 왼쪽이 기존 방식이고, 오늘쪽이 변경된 모습이다. 검색 결과 상단에 키워드 등록, 검색 필터, 거래 완료 안 보기 기능 등이 모여 있는 것이 복잡하다고 판단하여 유저의 탐색에 방해되지 않도록 서비스 UI를 개선하려 했다고 한다. </p>
</br>


<h1 id="abcd-테스트는-어떻게">ABCD 테스트는 어떻게?</h1>
<p>위 그림 예시처럼 두 가지 후보 중 하나를 골라야할 때, t-test로 실험을 설계하면 통계적 유의성을 검증하며 더 나은 후보를 선택할 수 있다. 그런데 만약 후보가 3개 이상일 때는 어떻게 실험을 설계할까? 이번 글에서는 AB 테스트의 실험 대상이 A와 B 두 집단만 있는 것이 아니라 A,B,C,D 처럼 여러 여러개의 후보가 있을 때, Muticlass AB Test 실험 설계 과정은 일반적인 AB 테스트와 어떤 차이점이 있는지 작성했다. </p>
</br>
</br>

<h1 id="1-예시--쿠폰-디자인-개선하기">1. 예시 : 쿠폰 디자인 개선하기</h1>
<p>유저에게 프로모션 쿠폰을 뿌린 다음, 유저가 그 쿠폰을 구매에 이용하는 비율을 쿠폰 환수율(Redemption Rate)이라고 부른다. 쿠폰 환수율을 높이기 위해 디자인 UI를 개선하고자 한다. 다음과 같은 후보가 나왔다.  </p>
<ul>
<li>기존 Design</li>
<li>Design A</li>
<li>Design B</li>
<li>Design C</li>
</ul>
<p><strong>redemption rate을 측정하는 방법</strong>은 다음과 같다. 
정해진 실험 기간동안 총 4 가지 후보 중 랜덤하게 디자인을 노출하고 각 유저 그룹마다 총 유저수(Target), 쿠폰 사용 수(Redeemed)를 기록한다. 분모를 Target, 그리고 분자를 Redeemed 으로 하여 Redemption Rate을 기록했고 그 결과는 다음과 같다. (이 예시는 <a href="https://towardsdatascience.com/the-ultimate-guide-to-multiclass-a-b-testing-3cca2c687bea" target="_blank"> 이 곳</a>에서 가져왔다.)</p>
<ul>
<li>Table 1
<img src="https://velog.velcdn.com/images/hong_journey/post/1fc71827-390f-4e40-9e5c-8ff43b5d056b/image.png" alt=""></li>
</ul>
</br>
</br>

<h1 id="2-실험-설계">2. 실험 설계</h1>
<h2 id="실험-목적">실험 목적</h2>
<ul>
<li>디자인 UI 를 개선해서 기존보다 쿠폰 환수율(Redemption Rate) 높이기</li>
</ul>
<h2 id="유의수준-정의">유의수준 정의</h2>
<ul>
<li>유의수준은 귀무가설을 기각할지 여부 결정하는 기준으로, 실험 결과인 p-value가 유의수준보다 작으면 귀무가설을 기각하고, 크면 기각하지 않는다.</li>
<li>따라서 유의수준은 p-value를 계산하기 이전에 미리 정한다. 보통은 유의수준을 0.05로 설정한다. </li>
</ul>
<h2 id="실험1-모든-디자인에서-쿠폰-환수율이-동일한가">실험1. 모든 디자인에서 쿠폰 환수율이 동일한가?</h2>
<ul>
<li>귀무가설 : 모든 디자인에서 쿠폰 환수 비율이 동일하다.</li>
<li>대립가설 : 적어도 하나의 디자인의 쿠폰 환수 비율 다르다. </li>
</ul>
<p>샘플로 수집한 쿠폰 환수 여부 데이터는 categorical 변수이다. (Redeemed / not Redeemed) 따라서 <strong>두 개 이상 그룹의 categorical 변수의 분포를 비교</strong>하는 테스트인 <strong>Chi-Squared Homogeneity Test</strong>를 사용한다. </p>
<p><strong>1. (Redeemed, not_Redeemed) 를 컬럼으로 하는 DataFrame 생성</strong></p>
<pre><code class="language-python">import pandas as pd

target = [8333, 8002, 8251, 8275] # existing, A, B, C
redeemed = [1062, 825, 1289, 1278]

not_redeemed = [] # target - redeemed
for a, b in zip(target, redeemed):
    not_redeemed.append(a-b)

data = pd.DataFrame({&#39;redeemed&#39; : redeemed, &#39;not_redeemed&#39; : not_redeemed})

</code></pre>
<p><strong>2. Chi-Squared Homogeneity Test</strong></p>
<pre><code class="language-python">from scipy.stats import chi2_contingency
# ref : https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.chi2_contingency.html

stat, p_value, df, expected = chi2_contingency(data)
print(stat, p_value)
#### 결과 : 131.88996393138655 2.117192550458385e-28</code></pre>
<p>테스트에서 사용할 검정통계량은 귀무가설 하에서 Chi-Squared 분포를 따르고, <code>scipy.stats</code> 라이브러리를 통해 검정통계량 값(<code>stat</code>)과 이에 해당하는 <code>p-value</code> 를 구할 수 있다. 결과를 보면 p-value값이 유의수준(0.05)보다 낮다. 따라서 귀무가설이 기각된다. </p>
<p>결론적으로 모든 디자인의 쿠폰 환수율이 같지 않다고 할 수 있다. 적어도 하나의 디자인이 쿠폰 환수율이 다르다고 판단하고 다음 스텝으로 넘어간다. </p>
<p>(만약 이 단계에서 귀무가설이 기각되지 않는다면? 다음 스텝으로 넘어가지 않고, 기존 디자인 UI를 유지하는 결론을 내릴 수 있다.)</p>
</br>
</br>

<h2 id="실험2-어떤-디자인의-쿠폰-환수율이-가장-높은가">실험2. 어떤 디자인의 쿠폰 환수율이 가장 높은가?</h2>
<p>쿠폰 환수율이 가장 높은 디자인 UI를 찾기 위해 t-test를 반복적으로 수행한다. </p>
<blockquote>
<ul>
<li>*<em>언제까지 반복? *</em><ul>
<li>통계적으로 유의한(significant) 결과를 얻기 전까지 반복</li>
</ul>
</li>
<li><strong>순서는 어떤 기준으로 정함?</strong> <ul>
<li>Table 1 에서 Design B &gt; Design C &gt; Existing Design &gt; Design A 순으로 쿠폰환수율이 높았다. 실험 목적은 &quot;쿠폰 환수율이 가장 높은 디자인&quot;을 찾는 것이므로, 가장 높은 환수율을 기록했던 B가 C보다 환수율이 높은지부터 테스트한다. </li>
</ul>
</li>
</ul>
</blockquote>
<p><strong>1. &quot;Design B vs Design C&quot; One-tailed t-test</strong></p>
<ul>
<li>귀무가설 : 두 그룹의 쿠폰 환수율이 같다. (Design B = Design C)</li>
<li>대립가설 : 디자인 B의 쿠폰 환수율이 더 높다. (Design B &gt; Design C)</li>
</ul>
<p>비율을 비교하는 테스트이므로 t-test for proportion을 사용한다. 두 그룹의 샘플 사이즈가 충분히 크기 때문에 CLT(중심극한정리)에 의해 z-test for proportion을 사용한다. </p>
<pre><code class="language-python">from statsmodels.stats.proportion import proportions_ztest

stat, p_value = proportions_ztest([1289, 1228], # redeemed Design B vs Design C
                                  [8251, 8275], # target Design B vs Design C
                                  alternative=&#39;larger&#39;)
print(p_value)
### 결과 : 0.08079746727160886</code></pre>
<p>검정통계량은 귀무가설 하에 z 분포를 따르고, <code>statsmodels.stats.proportion</code> 라이브러리를 통해 검정통계량 값(<code>stat</code>)을 구하고 이에 해당하는 <code>p-value</code> 를 구할 수 있다. 결과를 보면 p-value값이 유의수준(0.05)보다 높다. 따라서 귀무가설이 기각되지 않는다. </p>
<p>결론적으로 Design B와 Design C의 쿠폰환수율은 비슷한 수준이라고 말할 수 있다. (B가 C보다 쿠폰환수율이 높다고 말할 충분한 근거가 없다.)</p>
<p>(만약 이 단계에서 귀무가설이 기각된다면? Design B가 가장 높은 환수율이라고 결론내릴 수 있으므로 다음 스텝으로 넘어가지 않고 테스트를 마무리한다.)</p>
<ul>
<li>참고 : t 분포 대신 z 분포를 쓸 수 있는 이유
<img src="https://velog.velcdn.com/images/hong_journey/post/c06329d8-5f37-4c61-a5a2-7e3bb15d927b/image.png" alt=""></li>
</ul>
<p><a href="https://www.jmp.com/en_us/statistics-knowledge-portal/t-test/t-distribution.html" target="_blank"> 위 그림</a>은 z-분포(초록색)과 자유도(degrees of freedom) 에 따른 t-분포를 그린 결과이다. 자유도는 샘플사이즈에 비례한다. t-분포는 자유도가 커짐에 따라 z-분포에 근사해간다고 알려져있다. 또한 CLT라는 개념이 있는데, 주어진 데이터가 어떤 분포를 따르건, 표본 평균(샘플을 표본이라고 한다)의 분포는 z분포에 근사해간다는 정리(Theorem)이다. </p>
</br>

<p><strong>2. &quot;Design BC vs Existing Design&quot; One-tailed t-test</strong></p>
<p>Design B와 Design C를 한 그룹으로 합쳐서 Segment BC라는 새로운 Segment를 만든다. 그리고 B와 C 다음으로 환수율이 컸던 Existing Design 과 비교하는 One-tailed t-test 를 수행한다. </p>
<ul>
<li>귀무가설 : 두 그룹의 쿠폰 환수율이 같다. (Segment BC = Existing Design)</li>
<li>대립가설 : Segment BC의 쿠폰 환수율이 더 높다. (Design BC &gt; Existing Design)</li>
</ul>
<pre><code class="language-python"># Design B와 Design C를 한 그룹으로 합쳐서 Segment BC 
stat, p_value = proportions_ztest([1289+1228, 1062], # redeemed Segment BC vs Existing Design C
                                  [8251+8275, 8333], # target Segment BC vs Existing Design
                                  alternative=&#39;larger&#39;)
print(p_value)
### 결과 : 6.795738359244353e-08</code></pre>
<p>p_value가 유의수준(0.05)보다 낮으므로 귀무가설이 기각된다. 통계적으로 유의한(귀무가설 기각됨) 결과가 나왔으니 t-test는 다음 스텝으로 가지 않고 종료한다. </p>
<p>결론적으로 Design B와 Design C는 기존 디자인에서보다 쿠폰 환수율이 높다고 말할 수 있다. 또한 Design B와 Design C의 쿠폰 환수율은 비슷한 수준이다. (=차이가 유의하지 않다.) 따라서 둘 중 무엇을 Best UI로 선택할 것인지는 의사결정권자에게 달려있다. </p>
</br>
</br>

<h1 id="질문들">질문들</h1>
<p>여기까지가 간단한 예시를 통해 알아본 Multiclass AB Test 이고, 정리하다가 생긴 몇 가지 질문들을 정리해봤다. </p>
<h2 id="1-왜-t-test를-여러번-하면-안-되는가---실험1">1. 왜 t-test를 여러번 하면 안 되는가? - 실험1</h2>
<ul>
<li>귀무가설 : 모든 디자인에서 쿠폰 환수 비율이 동일하다.</li>
<li>대립가설 : 적어도 하나의 디자인의 쿠폰 환수 비율 다르다. </li>
</ul>
<p>예시처럼 디자인 후보가 4개라면, 이중 두가지를 고르는 경우의 수만큼 Multiple t-test를 수행하면 되지 않을까? 결론적으로 그렇게 하지 않는 이유는 <strong>테스트를 여러번 할수록 Type 1 Error가 증가</strong>하기 때문이다. </p>
<blockquote>
<p><strong>Type 1 Error</strong> = 귀무가설이 실제로 맞을 때, 귀무가설을 기각할 확률 </p>
</blockquote>
<p>4개의 그룹 중 2개를 고르는 조합의 수는 4C2=6 이다. 이렇게 총 6번의 t-test를 수행하기로 하고, 각각의 테스트는 서로 독립이며 유의수준은 0.05로 설정했다고 하자. 이 실험의 Type 1 Error를 계산하면 다음과 같다. </p>
<blockquote>
<p>*<em>Type 1 Error *</em>
$= P$(6번의 테스트에서 귀무가설이 하나라도 기각 |귀무가설이 실제로 맞음)
$= 1 - P$(6번의 테스트에서 귀무가설이 모두 기각 안 됨 | 귀무가설이 실제로 맞음)
$= 1 - (1-0.05)^6$ 
$= 0.265$</p>
<p>※ 1번의 테스트에서 $P$( 귀무가설이 기각 안 됨 | 귀무가설이 실제로 맞음) $= 1-0.05$  이다. 테스트끼리는 서로 독립이므로 테스트 숫자만큼 제곱한다.</p>
</blockquote>
<p>하나의 테스트를 수행할 때는 Type 1 Error가 0.05였다. 반면 6번의 테스트를 수행하는 실험에서는 Type 1 Error가 0.265로 높아진다. 귀무가설을 잘못 기각할 확률이 5%에서 26.5%로 높아진다는 말이다. 따라서 실험을 설계할 때는 테스트를 최대한 적게 세팅해야지 Type 1 Error 증가를 방지할 수 있다. </p>
<h2 id="2-여러-집단을-비교하니까-anova를-쓰면-안-되나---실험1">2. 여러 집단을 비교하니까, ANOVA를 쓰면 안 되나? - 실험1</h2>
<ul>
<li>귀무가설 : 모든 디자인에서 쿠폰 환수 비율이 동일하다.</li>
<li>대립가설 : 적어도 하나의 디자인의 쿠폰 환수 비율 다르다. </li>
</ul>
<p>실험 1에선 귀무가설과 대립가설을 정의하고 Chi-squared homogeneity test 로 실험 했다. 그런데 ANOVA 역시 여러 집단을 비교하는 통계 테스트 기법이다. </p>
<blockquote>
<p><strong>ANOVA : Analysis of Variance</strong></p>
</blockquote>
<ul>
<li>2개 이상 그룹의 평균을 비교하는 테스트</li>
<li>필요한 가정(Assumption)<ul>
<li>정규분포 따라야함. (히스토그램으로 분포 확인 혹은 Q-Qplot 정규성 검증으로 확인)</li>
<li>등분산성 만족</li>
<li>각 관측치는 독립 </li>
</ul>
</li>
<li>SST = SSW + SSB<ul>
<li>SSW : 그룹 내부의 관측치와 그룹 평균의 차이를 Sum of Square  </li>
<li>SSB : 그룹 평균과 전체 평균의 차이를 Sum of Square</li>
</ul>
</li>
<li>검정통계량 F = {SSB/df_B} / {SSW/df_w} 는 귀무가설 하에, F분포를 따름</li>
</ul>
<p>Chi-squared homogeneity test와 ANOVA의 차이는 무엇일까?</p>
<ul>
<li>공통점 : 두개 이상의 그룹에서, <strong>특정 변수의 분포</strong>가 동일한지 확인하는 테스트</li>
<li>차이점 : 그 변수가 Categorical 하다면 Chi-squared Test를, Continuous 변수가 포함되어 있다면 ANOVA 를 쓴다. </li>
</ul>
<p>간혹가다 데이터 전처리 과정에서 Continuous 변수처럼 보이지만 Categorical 변수인 경우가 있다. 예를 들어, 값이 0 또는 1로 구성되어 있지만 0보다 1이 큰 것이 중요하다는 숫자로서의 의미가 없다면, 해당 변수는 Categorical 변수이다. </p>
<h1 id="마무리하며">마무리하며</h1>
<h2 id="1-가정의-중요성">1. 가정의 중요성</h2>
<p>테스트를 선택하기에 앞서, 테스트의 가정이 맞는지 확인하는 것이 중요한 것 같다. 예를 들어, 특정 기능 도입이후 구매 전환율을 기록했다고 하자.  </p>
<p>아래 테이블에서, 구매 전환율은 대조군(Control)은 2%, 실험군(Test)에서 1.7% 로 0.3% 가량 감소했다. 이 0.3% 감소가 통계적으로 유의한 감소인지 확인하기 위해 테스트를 진행하려고 한다.</p>
<ul>
<li>Table 2 <a href="https://medium.com/towards-data-science/the-ultimate-guide-to-a-b-testing-part-4-non-parametric-tests-4db7b4b6a974" target="_blank">(출처)</a>
<img src="https://velog.velcdn.com/images/hong_journey/post/e5457d26-1879-44c8-b9ea-ff2f7d2061e8/image.png" alt=""></li>
</ul>
<p>주어진 데이터는 Payers와 Non-Payer 사이의 불균형이 심하다. 이렇게 카테고리가 불균형하거나 특정 그룹의 샘플 사이즈가 매우 작은 경우에는 Chi Square Test를 쓰지 않고 Fisher Exact Test를 쓴다. (Chi Square Test 가정 : 데이터가 불균형하지 않고 샘플 사이즈가 충분히 커야함.)</p>
<p>Python은 라이브러리가 잘 되어 있어서 AB Test를 할 때 검정통계량의 수식이 어떻게 구해지는 지에 대한 내용은 크게 중요하지 않아 보인다. 대신, 사용하려는 테스트의 가정이 주어진 샘플 데이터에서 만족되는지 체크하는 절차가 중요한 것 같다. 위 예시처럼 불균형이 심한 데이터의 경우, Chi Square Test의 가정을 만족하지 못하므로 대안으로 Fisher Exact Test를 써야한다. </p>
</br>


<h2 id="2-실험-목적의-중요성">2. 실험 목적의 중요성.</h2>
<p><strong>Multiclass AB Test에서 실험을 최대한 적게 하려면</strong> 실험 목적을 구체적으로 설정하는 것이 중요하다는 것을 알게 되었다. 위의 예시에서 &quot;만일 4가지 디자인의 쿠폰 환수율 순위&quot;를 구하는 것이 목적이었다면, 불필요한 t-test를 추가로 수행하여 Type 1 Error가 높아졌을 것이다. </p>
</br>
</br>

<h1 id="출처">출처</h1>
<ol>
<li><p><a href="https://towardsdatascience.com/the-ultimate-guide-to-multiclass-a-b-testing-3cca2c687bea" target="_blank">Multiclass AB Testing</a></p>
</li>
<li><p><a href="https://towardsdatascience.com/a-b-c-tests-how-to-analyze-results-from-multi-group-experiments-ad5d5cee0b05" target="_blank">ANOVA in ABC test</a></p>
</li>
<li><p><a href="https://towardsdatascience.com/the-ultimate-guide-to-a-b-testing-part-3-parametric-tests-2c629e8d98f8" target="_blank">Parametric Test</a> vs <a href="https://medium.com/towards-data-science/the-ultimate-guide-to-a-b-testing-part-4-non-parametric-tests-4db7b4b6a974" target="_blank">Non Parametric Test<a></p>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[AB 테스트 설계 용어(1)]]></title>
            <link>https://velog.io/@hong_journey/AB-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%84%A4%EA%B3%84-%EC%9A%A9%EC%96%B4</link>
            <guid>https://velog.io/@hong_journey/AB-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%84%A4%EA%B3%84-%EC%9A%A9%EC%96%B4</guid>
            <pubDate>Sun, 18 Sep 2022 14:35:12 GMT</pubDate>
            <description><![CDATA[<p>당근마켓 블로그를 읽다가 AB 테스트 내용이 포함된 <a href="https://medium.com/daangn/직관만-믿고-까불었다가-망한-pm의-사연-61946dc969eb" target="_blank"> 글</a>을 읽었다. </p>
<p>*<em>글을 요약하면, *</em>당근마켓에서 &quot;키워드 알림 등록 기능&quot;은 유저의 재방문에 큰 영향을 주는 기능인데, 중고 거래 검색 필터를 개편하면서 실험 없이 UI를 변경했다가 중요 지표인 &quot;키워드 알림 등록수&quot;가 급격히 감소했다는 내용이었다. 키워드 알림 UI를 변경한 후 알림 등록수가 하락한 원인을 분석하고, 이를 바탕으로 가설을 수립해 AB 테스트를 진행하여 결과를 분석하는 과정까지 설명되어있었다.</p>
</br>
</br>

<hr>
<p>글을 읽기 전, 그동안 내가 알던 AB 테스트는</p>
<p>1) 적절한 가설을 수립한 뒤 
2) 실험군/대조군을 정의하고 
3) 통제 변수, 샘플 크기, 실험 기간 설정하기. 
4) 실험
5) 대조군과 실험군의 지표 차이가 통계적으로 유의한지 검증하기</p>
<p>였는데 실험을 설계하는 과정에서 생각보다 고려해야할 요소들이 많았다. 예를 들어 이런 용어들이었다. </p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/d3cac62d-cff6-4b42-9510-3b1afca44557/image.png" alt=""></p>
</blockquote>
<ul>
<li>가드레일 지표?<ul>
<li>주요 지표 이외에 또 다른 지표를 왜 고려해야할까?</li>
</ul>
</li>
</ul>
<blockquote>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/1696df4e-7e47-4012-a784-a4afbf30642d/image.png" alt=""></p>
</blockquote>
<ul>
<li>신기 효과?<ul>
<li>실험 기간 직후 지표를 스냅샷해서 통계 테스트를 한다고 알고 있는데, 지표의 추이를 트래킹해야하는 이유가 무엇인가?</li>
</ul>
</li>
<li>ABCD 테스트를 할 때는 어떤 방법으로 테스트할까?</li>
</ul>
<hr>
</br>
</br>

<h2 id="1-ab-테스트는-왜-할까">1. AB 테스트는 왜 할까?</h2>
<p>우선, AB 테스트는 언제 쓸까? 타겟 유저의 행동을 정성, 정량적으로 분석하여 서비스의 개선사항을 발굴하는 과정에서 AB 테스트를 수행한다. 또 어떠한 의사결정을 하기 전에, 이 의사결정의 영향력을 측정하는 용도로도 쓰인다. </p>
</br>
</br>

<h2 id="2-가드레일-지표guardrail-metric">2. 가드레일 지표(Guardrail Metric)</h2>
<p>서비스의 핵심 지표 A의 개선을 위해 서비스의 UI를 변경하는 실험을 했는데, 예상치 못하게 서비스의 다른 중요한 지표 B가 떨어질 수 있다. B와 같은 지표를 <strong>가드레일 지표(Guardrail Metric)</strong>라고 한다. </p>
<p>현재 실험의 지표 개선에만 집중하다가, 떨어져서는 안 되는 중요 지표 성과가 낮아지는 문제가 생기지 않도록 실험 설계 과정에서 가드레일 지표를 설정하는 것이 중요하다. </p>
<p>서비스의 전체적인 성과를 1~2 개의 부분적인 지표 움직임으로는 판단할 순 없다. 따라서 현재 진행하는 실험이 유저에게 어떤 효과가 있었는지 여러가지 시나리오와 지표를 바탕으로 종합적으로 판단해야한다.  </p>
<p>그래서 지표는 세밀한 수준에서 트래킹하는 것이 중요하다고 한다. 데이터를 쪼개서 보는 것의 중요성은 심슨 패러독스 예시에서도 확인할 수 있다. (심슨 패러독스란 쪼개진 데이터에서 성립하는 관계가 합쳐진 데이터에서는 반대로 나타나는 현상을 말한다.) 예를 들어, 실험 결과로 &#39;키워드 알림 등록수&#39;가 상승 했다고 하더라도, OS 기기 별로 나누어서 지표 변화를 해석했을 때 집단별로 결과가 차이날 수 있다. </p>
</br>
</br>


<h2 id="3-신기효과novelty-effect">3. 신기효과(Novelty Effect)</h2>
<p>실험 직후 지표가 개선됨을 확인했으나, 장기적인 관점으로도 개선이라고 판단할 수 있을까? </p>
<p>새로운 기능을 배포하여 AB 테스트를 진행했을 때, 일시적으로 지표가 상승했더라도 실질적으로는 서비스 개선에 도움이 되지 않았을 수도 있다. 만일 그렇다면, 기능 늘리기 등의 불필요한 리소스를 투입하지 않아도 될 것이다. </p>
<p>이처럼 긍정적인 효과가 단기간에만 발생하고, 장기적으로 flat한 효과를 보이는 현상을 <strong>신기 효과(Novelty Effect)</strong>라고 한다. 
<img src="https://velog.velcdn.com/images/hong_journey/post/5abc1edb-4230-4061-af63-55ccf254760c/image.png" alt=""></p>
<p>위 그래프를 보면, 기능 도입 직후 지표가 일시적으로 증가했다가 다시 기능 도입 이전 수준으로 수렴된 것을 확인할 수 있다. 이렇게 일시적으로 튀는 <strong>신기 효과</strong>를 지나서, 지표가 다시 어느 수준에서 수렴되는지를 확인하면, 해당 기능이 서비스 개선에 도움이 됐는지 판단할 수 있다. </p>
</br>
</br>

<h2 id="마무리하며">마무리하며</h2>
<p>데이터 기반 의사결정의 과정에 대해 배울 수 있어 재밌는 <a href="https://medium.com/daangn/직관만-믿고-까불었다가-망한-pm의-사연-61946dc969eb" target="_blank"> 글</a>이었다. 
다음 글은 통계적 유의성을 확인하는 테스트 과정과,  <a href="https://velog.io/@hong_journey/AB-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%84%A4%EA%B3%84-%EC%9A%A9%EC%96%B4-2-Multiclass-AB-test-%EC%84%A4%EA%B3%84">&quot;ABCD 테스트는 어떻게 실험을 설계할까?&quot;</a> 에 대한 내용을 정리하려 한다. </p>
</br>
</br>

<hr>
<h2 id="참고한-자료들">참고한 자료들</h2>
<ol>
<li><p>당근마켓 팀블로그 글 : 직관만 믿고 까불었다가 망한 pm의 사연 <a href="https://medium.com/daangn/직관만-믿고-까불었다가-망한-pm의-사연-61946dc969eb" target="_blank"> 링크 </a></p>
</li>
<li><p>양승화 지음 / 그로스 해킹 / 위키 북스</p>
</li>
<li><p>A/B 테스트 결과 해석에서 자주 발생하는 12가지 함정들<a href="https://medium.com/bondata/a-b-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B2%B0%EA%B3%BC-%ED%95%B4%EC%84%9D%EC%97%90%EC%84%9C-%EC%9E%90%EC%A3%BC-%EB%B0%9C%EC%83%9D%ED%95%98%EB%8A%94-12%EA%B0%80%EC%A7%80-%ED%95%A8%EC%A0%95%EB%93%A4-2fe273b76a2d" target="_blank"> 링크 </a></p>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[데이터리안 8월 세미나 후기 / 이력서는 서비스 / 양승화 님 인터뷰]]></title>
            <link>https://velog.io/@hong_journey/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A6%AC%EC%95%88-8%EC%9B%94-%EC%84%B8%EB%AF%B8%EB%82%98-%ED%9B%84%EA%B8%B0-%EC%9D%B4%EB%A0%A5%EC%84%9C%EB%8A%94-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%96%91%EC%8A%B9%ED%99%94-%EB%8B%98-%EC%9D%B8%ED%84%B0%EB%B7%B0</link>
            <guid>https://velog.io/@hong_journey/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A6%AC%EC%95%88-8%EC%9B%94-%EC%84%B8%EB%AF%B8%EB%82%98-%ED%9B%84%EA%B8%B0-%EC%9D%B4%EB%A0%A5%EC%84%9C%EB%8A%94-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%96%91%EC%8A%B9%ED%99%94-%EB%8B%98-%EC%9D%B8%ED%84%B0%EB%B7%B0</guid>
            <pubDate>Sun, 04 Sep 2022 12:06:53 GMT</pubDate>
            <description><![CDATA[<p>인스타 광고를 보다 세미나에 그로스해킹의 저자인 양승화님 인터뷰가 포함되어있다고 해서 신청해봤다. 데이터리안이 어떤 일을 하는 곳인지는 잘 몰랐는데, 이번 기회에 <a href="https://www.datarian.io/blog">데이터리안 블로그</a>를 들여다보니 데이터 분석가 직무를 준비하면서 도움될 것 같은 자료들이 있었다. 세미나 비용은 1만원이었고, 세션 순서는 다음과 같았다.  </p>
<blockquote>
<h3 id="8월-세미나-주제--데이터-분석가-채용의-모든-것">8월 세미나 주제  &#39;데이터 분석가 채용의 모든 것&#39;</h3>
<ul>
<li>데이터리안의 이보민님의 &#39;이력서는 서비스여야 한다&#39;</li>
<li>마이리얼트립 양승화님의 &#39;데이터 분석 채용 이야기&#39;</li>
<li>데이터 분석가 6인의 질의응답</li>
</ul>
</blockquote>
</br>
</br>

<h1 id="1-이력서는-서비스여야-한다">1. 이력서는 서비스여야 한다</h1>
<blockquote>
<h4 id="느낀점">느낀점.</h4>
<ul>
<li>이력서를 적어도 한 달에 한번은 지속적으로 업데이트해야겠다</li>
<li>velog는 분석 툴이 없으니.. 네이버 블로그를 가지고 유입 경로, 유입 검색어 분석을 해봐야겠다. </li>
</ul>
</blockquote>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/bb1e88f8-d3cd-494a-927b-54487cf9dd77/image.png" alt=""></p>
<p>강연자 : 이보민님(데이터리안 데이터 분석가)
강연 대상 : </p>
<ul>
<li>데이터 역량을 어필하고 싶은 모든 직군</li>
<li>이력서 순서가 어려운 구직자</li>
<li>분석 프로젝트 데이터를 어디서 얻고 어떻게 진행해야할지 궁금한 사람</li>
<li>데이터 분석가로 직무전환할 때 사이드 프로젝트로 어떤 활동을 추천하는지 궁금한 사람</li>
</ul>
<p>강의 슬라이드는 <a href="https://speakerdeck.com/datarian/deiteo-bunseogga-caeyongyi-modeun-geos-iryeogseobuteo-myeonjeobggaji-weolgan-deiteorian-semina-8weol">이곳</a>에 있습니다.</p>
<h2 id="11-이력서를-왜-웹서비스로-만들었나요">1.1 이력서를 왜 웹서비스로 만들었나요?</h2>
<blockquote>
<p>이전 직장이 채용 플랫폼이다보니 채용 서비스를 업그레이드하기 위해 다양한 채용 공고와 경로를 찾아보게 되었는데, 사이트별로 이력서를 따로 등록해야하는 것이 구직자 입장에서 귀찮은 일이라고 생각했다. </p>
<p>그래서 노션 웹으로 만들어서 페북으로 공유해봤는데 생각보다 반응이 있어서, oopy로 데이터 분석툴을 연동시켜 데이터를 확인해보기로 했다. </p>
</blockquote>
<h2 id="12-사람들이-내-이력서를-어떻게-보고-있는지-알고-싶다">1.2 사람들이 내 이력서를 어떻게 보고 있는지 알고 싶다</h2>
<blockquote>
<p>이력서 데이터를 보고 개선점을 찾기 위해 사용자 행동 데이터를 확인했다.  (ex. 웹사이트 페이지뷰, 스크롤, 클릭 등의 사용자 행동 패턴 파악기)</p>
<p>이력서도 마치 서비스처럼, 사용자들이 내가 설계한 경험 루트에 따라 최종 목적지까지 도달하고 있는지 확인하기 위해 직접 퍼널을 설계하고  <a href="https://www.datarian.io/blog/funnel-analysis#a261d1d8-5d4b-478b-b122-cd9aa2703066:~:text=%ED%8D%BC%EB%84%90%20%EB%B6%84%EC%84%9D%20(Funnel%20Analysis)">퍼널 분석</a>을 수행했다. </p>
<ul>
<li>이 이력서의 목적 : 이 이력서를 보는 사람들이 이직이나 강연을 제안</li>
</ul>
</blockquote>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/f9f0b318-3093-4c1b-ac66-d35fbd347b6a/image.png" alt=""></p>
<h2 id="13-분석">1.3 분석</h2>
<p>다음은 보민님이 설계한 이력서 사이트 퍼널의 분석 결과이다.</p>
<h3 id="퍼널1-이력서-사이트-접속">퍼널1) 이력서 사이트 접속</h3>
<h4 id="q-얼마나-많은-사람들이-어떤-사람들이-어느-경로로-방문">Q. 얼마나 많은 사람들이, 어떤 사람들이, 어느 경로로 방문?</h4>
<ul>
<li>최근 28일 접속자 추이를 먼저 확인. </li>
<li>기기별 사용자 분포를 확인(Desktop:87.1%, Mobile:11.1%, Tablet:1.7%)하니, 모바일쪽 UI는 신경 별로 안 써도 되겠다고 판단.</li>
<li>홍보 리소스 대비 유입량 많은 채널 확인 (최근 14일)<ul>
<li>direct/none 이 가장 많았다. (식별 불가능한 유입)</li>
<li>이외 식별 가능한 채널을 확인했을 때<ul>
<li>구글 검색으로 들어오는 트래픽이 가장 많음. (organic : 자연적으로 발생한 유입. 보통 검색으로)</li>
<li>datarian 블로그 유입</li>
<li>유튜브를 통해서도 유입됨</li>
</ul>
</li>
</ul>
</li>
</ul>
</br>


<h3 id="퍼널2-이력서-열람">퍼널2) 이력서 열람</h3>
<h4 id="q-스크롤은-어느정도-세부-내용은-클릭했을까">Q. 스크롤은 어느정도? 세부 내용은 클릭했을까?</h4>
<ul>
<li>10% / 50% / 80% 스크롤 기준으로 이벤트 수, 총 사용자, 페이지뷰 대비 전환율을 확인. (최근 14일)</li>
<li>click 데이터로 페이지뷰 대비 클릭 전환율을 확인하여, 접속자 중에서 상세 내용을 클릭해본 사용자가 얼마나 많은지 확인. </li>
</ul>
</br>

<h3 id="퍼널3-연락처email-sns클릭">퍼널3) 연락처(email, SNS)클릭</h3>
<h4 id="q-최종-목적지에-잘-도달하고-있는가">Q. 최종 목적지에 잘 도달하고 있는가?</h4>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/4e357743-5a69-4ecf-91bd-50edc03a6374/image.png" alt=""></p>
<ul>
<li>전체적으로 링크드인 클릭수가 가장 높았음. (최근 14일)</li>
<li>유입 채널별로 어떻게 다른지도 확인 가능.</li>
<li>전환수가 가장 많은 유입경로는 구글 검색. <ul>
<li>하지만 전환율(접속자 수 대비 유입 전환 수)은 유튜브 채널이 높았음. </li>
</ul>
</li>
</ul>
</br>


<h3 id="그래서-어떻게-개선하지">그래서 어떻게 개선하지?</h3>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/9a113d4f-4fe0-463e-91ff-4aea0d565f5a/image.png" alt="">
퍼널 단계별 전환율은 위와 같은데.. 어떤 지표를 개선하지?</p>
<h4 id="1-전환율을-높여보자">1. 전환율을 높여보자.</h4>
<ul>
<li>&quot;이메일 보내기&quot; CTA 버튼 클릭 전환율이 낮았음. 덜 부담스럽게 문구 변경</li>
<li>직접 메일 보내는 링크들을 빼고 링크드인으로 일단 일촌 신청 유도하기</li>
</ul>
<h4 id="2-이력서-세부사항까지-읽게-해보자">2. 이력서 세부사항까지 읽게 해보자.</h4>
<p>처음에는 이력서 페이지 이탈을 우려해서 세부사항은 토글로 만들고 이벤트를 심어놓았는데, 많이 클릭하진 않았다. 하지만 이 이력서를 보는 사람이 이런 세부 내용까지 잘 확인했으면 좋겠어서 토글 대신 펼쳐놓기로. </p>
<ul>
<li>이후 모니터링할 지표 : 스크롤 비율에 변화가 있는지</li>
</ul>
<h4 id="3-신규-유입경로를-발굴해보자">3. 신규 유입경로를 발굴해보자.</h4>
<p>유입 경로별 전환율을 확인했을 때, 유튜브가 제일 전환율이 높았다. 
(이걸 근거로 어떻게 신규 유입 경로를 발굴할 수 있는 건지는 잘 이해가 되지 않았음)</p>
</br>

<h2 id="14-이력서-웹서비스처럼-만들어야하는-이유-4가지">1.4 이력서, 웹서비스처럼 만들어야하는 이유 4가지</h2>
<blockquote>
<ol>
<li>보는 사람을 고려해서 내용을 작성하게 된다.</li>
<li>이력서의 실패는 나의 실패가 아니다. </li>
<li>퍼스널 브랜딩을 할 수 있다. </li>
<li>이력서 만드는 과정을 나만의 프로젝트로 만들 수 있다. </li>
</ol>
</blockquote>
<p>이부분에서 인상깊었던 것이, (타겟으로 설정한) 인사담당자의 입장을 고려해서 이력서를 작성할 수 있다는 것이다. 머리로 이해하더라도 그게 쉽지 않기 때문에, 차라리 이력서를 웹서비스처럼 만들다보면 인사담당자 분들이 어떤 식으로 페이지를 읽고 어떤 생각을 할지 조금 감이 잡힐 수도 있다는 것이다. </p>
<p>이력서를 웹서비스처럼 만들면, 내 이력서에 어떤 문제가 있는지 실마리를 찾기가 쉬워질 수도 있다. 이를 위해 이력서를 한 번 업데이트해서 그치는 게 아니라 지속적으로 수정하면서 지표를 확인해야한다. </p>
</br>
</br>

<h1 id="2-마이리얼트립-양승화님의-데이터-분석-채용-이야기">2. 마이리얼트립 양승화님의 &#39;데이터 분석 채용 이야기&#39;</h1>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/906b1711-5798-45d3-992c-f74d374671ce/image.png" alt=""></p>
<p>질답형식으로 이뤄진 시간이었는데 신입 분석가로 지원하는 과정에서 마침 고민하고 있던 내용들이 많았다.</p>
</br>

<h3 id="q-이력서를-차별화하려면">Q. 이력서를 차별화하려면?</h3>
<blockquote>
<p>채용을 하다보면, 뽑는 사람입장에서 비슷한 이력서와 경험이 많다. 뽑는 입장에서는, 결과물보다는 왜 그런 분석을 했는지, 혹은 왜 그런 교육 과정을 선택했는지가 더 궁금하다. 경력 채용이라면 스킬셋보다는 실제로 어떤 성과를 내고 어떤 임팩트를 만들어봤는지가 중요하다. </p>
</blockquote>
<p>최근에 봤던 한 면접에서 내 프로젝트 경험에 대해 &#39;구체적으로 어떤 문제를 풀고자 했는지, 이 결과로 어떤 임팩트를 만들어낼 수 있을지&#39;를 물어보셨다. 나는 &#39;데이터를 들여다보다가 이러이러한 특징을 발견했고, 그걸 활용해 결과(ex. 모델 성능)를 이만큼 개선했다&#39;와 같이 답변했는데, 말하고 보니 질문 의도와 다른 답변이었다. </p>
<p>데이터 분석가에게는 구체적인 숫자보다는, 지표를 어떻게 정의했는지, 왜 그렇게 기준을 잡았는지, 그리고 어떻게 지표를 측정했는지가 더 중요한 것 같다는 느낌을 받았다. 지난 프로젝트들을 회고하면서 이부분을 다시 정리하고 있는데 쉽지 않은 것 같다.</p>
</br>

<h3 id="q-데이터-분석가-채용-면접에서-특히-중요하게-보는게-있는지-그걸-검증해보기-위해서-특별히-해보는-질문이-있는지">Q. 데이터 분석가 채용 면접에서 특히 중요하게 보는게 있는지, 그걸 검증해보기 위해서 특별히 해보는 질문이 있는지?</h3>
<blockquote>
<ul>
<li><p>우선 채용은 (드러나지 않은) 상황이 많이 작용한다고 생각한다. 팀에 필요한 사람을 찾다보니 그렇다. </p>
</li>
<li><p>면접 때는 <strong>지원자를 움직이게 하는 키워드</strong>가 무엇인지를 많이 물어본다. (성장인지, 좋은 동료인지, 특정 카테고리의 서비스인지, 혹은 특정 경험인지..) </p>
</li>
<li><p>그래서 이전에 어떤 성과를 냈는지를 많이 보는 것 같다. 이전 회사를 왜 떠나려고 하는지도 질문한다. </p>
</li>
<li><p>예를 들어, 시니어 분석가가 있는 곳에서 배우고 싶은 지원자라면. 채용 담당자 입장에선.. 우리 회사에 시니어가 다 사라지면, 떠날 사람인가? 하는 생각이 들 수도 있다. 만약 &#39;시니어가 있으면 이런 임팩트를 더 낼 수도 있을 것 같다.&#39;고 하면 납득 가능한 이유일 것 같음. </p>
</li>
</ul>
</blockquote>
<p>이와 비슷하게, &#39;내가 가진 프로젝트 이력이 어필이 가능할까?&#39;는 구직자 입장의 질문이고, &#39;이 프로젝트의 문제들이 회사에서 필요한 문제일까?&#39;는 채용자 입장의 질문인 것 같다. 그래서 내가 지원하는 회사가</p>
<ul>
<li>어떤 데이터를 가지고 있고</li>
<li>어떤 문제를 풀고자 하는지</li>
</ul>
<p>직접 물어보거나, 물어볼 수 없다면 내가 스스로 가정을 해서 내가 이러한 부분을 기여할 수 있다고 말해야겠다. </p>
</br>

<h3 id="q-공부하는-방식에-대해">Q. 공부하는 방식에 대해</h3>
<blockquote>
<p>공부에도 단계가 있다고 생각한다. </p>
<ul>
<li>1단계 : 책으로 몰랐던 것을 알게됨.</li>
<li>2단계 : 배운 것을 써먹어봄. 가능하면 업무에 적용해보기.</li>
<li>3단계 : 적용한 것을 기록하고 공유하기. </li>
</ul>
<p>그래서 공부한 것을 블로그 글로 정리하거나, 세미나로 강의하는 것을 권장하는 편. </p>
<p>책 추천</p>
<ul>
<li>린 분석(엘리스테어 크롤, 벤저민 요스코비츠 저)</li>
<li>빅데이터를 활용한 예측마케팅 전략(외머 아튼, 도미니크 레빈 저)</li>
<li>틀리지 않는 법(조던 엘렌버그 저)</li>
<li>디맨드 (에이드리언 슬라이워츠키, 칼 웨버 저)</li>
<li>진화된 마케팅 그로스 해킹 (션 엘리스, 모건 브라운 저)</li>
</ul>
</blockquote>
</br>

<h3 id="q-마지막으로-데이터-분석가-채용을-하고-계시는-시니어-분들에게-그리고-지원을-막-해보고-있는-주니어-분들에게-각각-한-말씀-부탁드리면">Q. 마지막으로 데이터 분석가 채용을 하고 계시는 시니어 분들에게, 그리고 지원을 막 해보고 있는 주니어 분들에게 각각 한 말씀 부탁드리면</h3>
<blockquote>
<ul>
<li>(주니어에게 드리고 싶은 말씀은) 데이터 분석은 문제 해결이다. 문제 해결 방법, 더 나아가서 좋은 문제 찾기가 중요하고.. 이 부분에 초점을 맞춰서 이력을 쌓아가고 어필하면 채용에 좋은 결과가 있을 것이라 생각함. </li>
<li>완벽하지 않은 환경이라도 주어진 여건에서 최소한 어떤 것을 이루어냈고, 어떤 것을 하려고 시도했는지를 중요하게 본다. </li>
<li>(시니어분들에게 드리고 싶은 말씀은) 데이터 분석은 뛰어난 개인이 하기에는 한계가 있다고 생각. 그래서 조직이 데이터로 일하는 환경인지, 잘 받아들이는 문화인지가 중요하다고 생각함. 이런 부분을 신경쓰시면 좋을 것 같다. </li>
</ul>
</blockquote>
</br>


<h1 id="마무리하며">마무리하며</h1>
<p>호기심에 세미나를 신청했는데, 뜻밖의 회고를 할 수 있어 좋은 시간이었던 것 같다. 이전 프로젝트들을 정리하면서 &#39;지금 했다면 무엇을 다르게 했을 것인지&#39; 이전 분석의 문제점을 정리하는 글도 써봐야겠다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SQL, 데이터를 배열로 저장하는 이유]]></title>
            <link>https://velog.io/@hong_journey/SQL-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-%EB%B0%B0%EC%97%B4%EB%A1%9C-%EC%A0%80%EC%9E%A5%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hong_journey/SQL-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-%EB%B0%B0%EC%97%B4%EB%A1%9C-%EC%A0%80%EC%9E%A5%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 21 Aug 2022 14:43:19 GMT</pubDate>
            <description><![CDATA[<p>캐글의 SQL <a href="https://www.kaggle.com/code/alexisbcook/nested-and-repeated-data">튜토리얼</a>을 공부하다 Nested, Repeated 데이터타입을 알게되었다. </p>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/dbf07ff4-ffba-4f59-a21a-648ca3a1056a/image.png" alt="">
관측치로 하나의 스칼라 값이 들어간 것이 아니라, 마치 dict 타입처럼 저장된 방식이었다. (위 예시에선 <code>totals</code> column처럼) </p>
<p>왜 이러한 형태로 저장하는지, 그리고 <code>totals</code> column에 들어 있는<code>&#39;visits&#39;</code>,<code>&#39;hits&#39;</code>,<code>&#39;pageviews&#39;</code> 데이터는 SELECT문으로 어떻게 조회할 수 있을지 정리했다. </p>
<p>글의 순서는 다음과 같다. </p>
<ol>
<li>Nested Data란?</li>
<li>Repeated Data란?</li>
<li>Nested &amp; Repeated Data란?</li>
<li>데이터를 배열로 저장하는 이유</li>
</ol>
<h1 id="1-nested-data">1. Nested Data</h1>
<ul>
<li>datatype : <strong>RECORD</strong> (<strong>STRUCT</strong> 라고도 표현함)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/1f589f5e-cc3a-429f-b697-2498c79b4345/image.png" alt="">
<code>pets</code>, <code>toys</code> 테이블이 있다고 가정하자. </p>
<ul>
<li><code>pets</code>는 동물 정보가 담긴 테이블이고, <code>toys</code>은 동물이 가지고 노는 장난감 정보가 있다.   </li>
</ul>
<p>이 두 테이블을 오른쪽처럼 하나의 테이블 <code>pets_and_toys</code>로 합칠 수 있다. <code>pets_and_toys</code>의 &quot;Toy&quot; 칼럼안에 <strong>Name</strong>, <strong>Type</strong> 필드가 동시에 포함된 형태를 &quot;Nested 되어있다&quot;고 표현한다. </p>
<pre><code class="language-sql">-- &quot;pets_and_toys&quot; 테이블 생성
SELECT
    p.ID,
    p.Name,
    p.Age,
    p.Animal,
    STRUCT(t.Name, t.Type) Toy
FROM
    pets p
    LEFT JOIN toys t ON p.ID = t.Pet_ID</code></pre>
<p>데이터 접근 방법</p>
<ul>
<li>Nested Data에 포함된 Name 값은, <code>Toys.Name</code>의 형식으로 접근할 수 있다. </li>
<li>예시
<img src="https://velog.velcdn.com/images/hong_journey/post/8a224291-73c7-49f6-831f-749bec6ff9a7/image.png" alt=""></li>
</ul>
</br>
</br>



<h1 id="2-repeated-data">2. Repeated Data</h1>
<ul>
<li>datatype : <strong>REPEATED</strong></li>
</ul>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/6cca4a60-f6b1-40cb-956d-795fcbf03de6/image.png" alt=""></p>
<p>만약 동물(pets)마다 가지고 있는 장난감(toys)이 여러개라면? Repeated 타입을 쓸 수 있다.  <code>pets</code> 테이블과 <code>toys_type</code> 테이블을 합쳐서, 오른쪽 <code>pets_and_toys_type</code>과 같은 테이블을 생성할 수 있다. </p>
<pre><code class="language-sql">-- &quot;pets_and_toys_type&quot; 테이블 생성

--- (1) 임시 테이블 repeated_toys 생성 : repeated column인 Toys 만들기
WITH repeated_toys AS(
    SELECT
        Pet_ID,
        ARRAY_AGG(Type ORDER BY ID) Toys
    FROM
        toys_type
    GROUP BY
        Pet_ID
)

--- (2) LEFT JOIN
SELECT
    p.ID,
    p.Name,
    p.Age,
    p.Animal,
    r.Toys
FROM
    pets p
    LEFT JOIN repeated_toys r ON p.ID = r.Pet_ID</code></pre>
<p>(1) 임시 테이블 repeated_toys 생성 : nested column인 Toys 만들기</p>
<ul>
<li>[Frisbee, Bone, Rope] 와 같은 Repeated Data를 생성하기 위해서 <code>ARRAY_AGG(col)</code> 함수를 사용한다. <ul>
<li><code>ARRAY_AGG(col)</code>는 ARRAY(배열)을 리턴한다. </li>
<li>Repeated 필드인 Toys에 입력된 값들은 ARRAY(배열) 데이터이다.</li>
<li>배열 데이터는 순서가 있고(ordered list), 동일한 datatype으로 구성되어있다. </li>
</ul>
</li>
<li>Pet_ID를 기준으로 배열 데이터(Repeated Data)를 생성하고자 하므로 <code>GROUP BY Pet_ID</code> 구문이 필요하다. </li>
</ul>
<p>(2) LEFT JOIN</p>
<ul>
<li>pets 테이블과 repeated_toys 테이블을 Pet_ID 를 기준으로 LEFT JOIN 한다. </br>


</li>
</ul>
<p>데이터 접근 방법</p>
<ul>
<li>Toys 내부의 배열 데이터에 접근하려면, <code>UNNEST</code> 함수(in Bigquery)를 이용한다. (<code>UNNEST</code> 함수는 이어지는 3장에서 정리)</li>
<li>예시
<img src="https://velog.velcdn.com/images/hong_journey/post/70a48f66-686d-471b-b33c-0a8dbeac5f55/image.png" alt=""></li>
</ul>
</br>
</br>


<h1 id="3-nested-and-repeated-data">3. Nested and Repeated Data</h1>
<ul>
<li>datatype : <strong>RECORD and REPEATED</strong></li>
</ul>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/73b8c175-f7b0-4caf-b588-8bc8e1ce7b11/image.png" alt=""></p>
<p>Nested 되어있으면서 Repeated 형태의 데이터 타입도 가능하다. 오른쪽 <code>more_pets_and_toys</code> 테이블의 Toys 필드는 Nested and Repeated Data이다. </p>
<pre><code class="language-sql">-- &quot;more_pets_and_toys&quot; 테이블 생성
--- (1) 임시테이블 nested_repeated_toys 생성 
WITH nested_repeated_toys AS(
    SELECT
        Pet_ID,
        ARRAY_AGG(
            STRUCT(Name, Type) 
            ORDER BY 
                ID
           ) Toys
    FROM
        more_toys
    GROUP BY
        Pet_ID
)

--- (2) LEFT JOIN
SELECT
    p.ID,
    p.Name,
    p.Age,
    p.Animal,
    n.Toys
FROM
    pets p
    LEFT JOIN nested_repeated_toys n ON p.ID = n.Pet_ID</code></pre>
<p>Toys 칼럼의 datatype은,</p>
<ul>
<li><strong>RECORD</strong> : <code>Name</code>, <code>Type</code> 값이 Nested 되어있다. </li>
<li><strong>REPEATED</strong> : <code>Toys.Name</code>과 <code>Toys.Type</code> 은 각각 ARRAY이다. </li>
</ul>
</br>




<p>데이터 접근 방법</p>
<ul>
<li>예시
<img src="https://velog.velcdn.com/images/hong_journey/post/5b041ac3-cd67-4bb5-a015-bc839720b27d/image.png" alt=""></li>
</ul>
<h4 id="unnestcol-는"><code>UNNEST(col)</code> 는?</h4>
<ul>
<li>Repeated 칼럼을 Flatten 하는 함수다. </li>
<li>인자로 들어가는 col은 ARRAY 타입이어야한다. (<code>[]</code>로 감싸진 형태)<ul>
<li>가능 (O) 예시 : <code>[{&#39;Name&#39;:&#39;A&#39;, &#39;Type&#39;:1}, {&#39;Name&#39;:&#39;B&#39;, &#39;Type&#39;:2}]</code> <ul>
<li>가능 (X) 예시 : <code>{&#39;Name&#39;:[&#39;A&#39;,&#39;B&#39;], &#39;Type&#39;:[1, 2]}</code> </li>
</ul>
</li>
</ul>
</li>
<li>UNNEST 함수는 FROM 절에서 사용한다.</li>
<li>Alias를 쓰면 값을 조회하기 편하다. <ul>
<li>위 예시에는 Alias를 t로 세팅했고, SELECT 문에서 <code>t.Name</code>, <code>t.Type</code> 형태로 조회했다.</li>
</ul>
</li>
</ul>
</br>

<blockquote>
<h4 id="nested-and-repeated-data-예시">Nested and Repeated Data 예시</h4>
<pre><code class="language-sql">WITH nested_repeated_toys AS(
    SELECT
        Pet_ID,
        ARRAY_AGG(
            STRUCT(Name, Type) 
            ORDER BY 
                ID
           ) Toys
    FROM
        more_toys
    GROUP BY
        Pet_ID
)</code></pre>
<p><strong>예시 1</strong>: <code>[{&#39;Name&#39;:&#39;A&#39;, &#39;Type&#39;:1}, {&#39;Name&#39;:&#39;B&#39;, &#39;Type&#39;:2}]</code>
<strong>예시 2</strong>: <code>{&#39;Name&#39;:[&#39;A&#39;,&#39;B&#39;], &#39;Type&#39;:[1, 2]}</code></p>
<ul>
<li><code>nested_repeated_toys</code> 테이블의 Toys 칼럼은 <strong>&lt;예시 1&gt;</strong> 형태다.</li>
<li>STRUCT 함수와 ARRAY_AGG 함수의 순서에 따라 &lt;예시 1&gt;이 될 수도 있고, &lt;예시 2&gt;가 될 수도 있다. </li>
<li>Nested and Repeated Data의 예시를 살펴보면 &lt;예시 1&gt;의 형태가 더 많은데, 왜 그럴까?</li>
<li>(추측으로) &lt;예시 2&gt;라면 조회하기 어려워서 그런 것 같다. &lt;예시 2&gt;는 배열이 두 개 (&#39;Name&#39;과 &#39;Type&#39;에 각각 하나씩) 있기 때문에, 각각의 배열을 Flatten하기 위해 UNNEST 함수를 두 번 써야한다. 반면, &lt;예시 1&gt;은 UNNEST를 한 번만 쓰면 된다.</li>
</ul>
</blockquote>
</br>
</br>



<h1 id="4-데이터를-배열로-저장하는-이유">4. 데이터를 배열로 저장하는 이유</h1>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/564a8ec7-3435-43be-a552-3cccd3fba0cd/image.png" alt=""></p>
<p>왼쪽 테이블은 Toys 칼럼에 배열을 사용했고, 총 3개 행으로 이루어져 있다.
오른쪽 테이블은 배열을 사용하지 않았고, 총 6개 행으로 이루어져있다. </p>
<p>처음에는 왜 굳이 배열 데이터를 써야하는지 이해가 잘 가지 않았다. 애초에 데이터가 <strong>이미 배열 형태로 저장되어있는 경우</strong>라면, UNNEST 와 같은 함수를 이용해서 데이터를 조회가능하다는 것은 이해가 갔다.</p>
<p>하지만 <strong>데이터를 저장하는 관점</strong>에서는, 배열을 사용하는 것이 더 복잡도가 높아질 것이라고 생각했다. (만약 Repeated and Nested and Repeated and Nested, ... 형태라면..? SELECT문으로 조회하려면 굉장히 복잡해질 것 같다..)  </p>
<p>&lt;구글 빅쿼리 완벽 가이드&gt; 책에서 관련 내용을 찾아보니, 배열 형태로 데이터를 저장하는 몇 가지 상황을 알게 되었다.</p>
</br>

<h2 id="41-데이터의-순서가-중요한-경우">4.1 데이터의 순서가 중요한 경우</h2>
<p>분석 작업을 위해 데이터를 가공해 테이블로 저장해두었다고 했을 때, 나중에 그 테이블을 읽을 때 정렬이 유지된다는 보장이 없다. 분석 작업에서 데이터의 순서가 중요하다면, 순서를 저장하기 위해 배열을 사용하기도 한다. (위의 예시의 경우, Toy_Name과 Toy_Type은 순서가 크게 중요하지 않아보인다.)</p>
<h2 id="42-반복-가능성-있는-값들을-단일-행에-저장하기">4.2 반복 가능성 있는 값들을 단일 행에 저장하기</h2>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/564a8ec7-3435-43be-a552-3cccd3fba0cd/image.png" alt=""></p>
<p>(오른쪽과 달리) 왼쪽 테이블은 pet_ID마다 하나의 행이 보장된다. 배열을 사용하면, 컬럼 간에 일대일 관계를 유지할 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/b205a929-1869-4d7d-8f6e-a527e771b42e/image.png" alt=""></p>
<p>단일 행으로 저장하면 JOIN을 사용할 때도 편하다. 위 두 테이블에 각각 새로운 테이블 <code>Snack</code>을 JOIN 한다고 가정해보자. (key값 : Pet_ID)</p>
<ul>
<li>Case 1의 경우, 1:N 대응으로 JOIN을 수행할 수 있지만</li>
<li>Case 2의 경우, N:N 대응으로 JOIN을 수행해야한다. Pet_ID를 기준으로 Snack Name을 매칭한 결과, 행의 개수가 크게 늘어날 것이다.</li>
</ul>
<p>책에 언급된 또 다른 예시는 아래와 같다.</p>
<blockquote>
<p>*<em>A지역에 위치한 기관들의 세금 신고 기록 데이터가 있다고 하자. *</em>
매년 한 번 세금을 신고하는 기관이 있는 반면, 같은 해 여러 번 세금을 신고하는 기관도 있다. 만일 다음 번에 이 테이블을 조회할 때, 같은 해에 여러 번 세금을 신고하는 기관도 있다는 사실을 잊어버리면 문제가 생길 수가 있다. </p>
</blockquote>
</br>

<p>어떤 문제가 발생할 수 있을까? 다시 <code>Pets</code>,<code>Toys</code> 테이블로 돌아와 간단한 예시를 들어보겠다. </p>
<p>어느날 예기치 못한 사고(?) 로 동물들이 가지고 있던 장난감을 모두 잃어버렸다. 그리고, Toys 정보에 <code>Toys_Onsale</code> 이라는 정보를 추가했다고 하자.</p>
<ul>
<li><code>Toys_Onsale</code> : 장난감이 현재 쇼핑몰에 판매중인지 여부 (구매가능:1, 구매불가:0)</li>
</ul>
<p>동물들마다 원래 가지고 있던 장난감을 다시 사주려고 한다. 이때, 가지고 있던 장난감이 모두 단종(<code>Toys_Onsale=0</code>)된 동물이 있을까? </p>
<h4 id="배열-사용-x-경우">배열 사용 (X) 경우</h4>
<p>[ 테이블 명: example ]</p>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/01759a5f-b0d6-4445-97e3-c0b640774927/image.png" alt=""></p>
<p>장난감이 구매불가한 조건은 <code>Toys_Onsale = 0</code> 이다.
간단하게는 다음과 같은 쿼리를 작성할 수 있을 것이다. </p>
<pre><code class="language-sql">SELECT
    DISTINCT Pet_ID
FROM
    example 
-- pets가 가지고 있는 toy가 구매 불가한 경우(Toys_Onsale=0)
WHERE
    Toys_Onsale = 0 </code></pre>
<ul>
<li>결과</li>
</ul>
<p><img src="https://velog.velcdn.com/images/hong_journey/post/962fa75b-af68-4139-a95d-cb88c387cd5f/image.png" alt=""></p>
<p>하지만 일부 동물은 구매가능한 장난감과 구매불가한 장난감을 동시에 가지고 있기도 하다. (Pet_ID가 1인 동물은 구매가능한 장난감 2개, 구매불가능한 장난감 1개를 가지고 있다.)</p>
<p>&quot;모든 장난감이 구매 불가한 동물&quot;을 찾기 위해서는 어떻게 해야할까? 다음과 같이 배열을 사용해서 확인할 수 있다. </p>
<h4 id="배열-사용-o-경우">배열 사용 (O) 경우</h4>
<pre><code class="language-sql">-- &quot;nested_repeated_toys&quot; : Toys 칼럼에 배열을 사용 
WITH nested_repeated_toys AS (
    SELECT
        Pet_ID,
        ARRAY_AGG(
            STRUCT(Name, Type, On_sale)
            ORDER BY
                ID
        ) Toys
    FROM
        more_toys
    GROUP BY
        Pet_ID
)

-- 장난감이 모두 구매 불가 상태인 Pet_ID 조회
SELECT
    DISTINCT Pet_ID
FROM
    nested_repeated_toys
WHERE
    1 NOT IN (
        SELECT
            On_sale
        FROM
            UNNEST(Toys)
    )</code></pre>
<p>WHERE 절에 있는 서브쿼리 <code>SELECT On_sale FROM UNNEST(Toys)</code> 는 아래와 같다. <img src="https://velog.velcdn.com/images/hong_journey/post/61478a03-5343-4028-85e7-c74899ec36c9/image.png" alt="">
여기서 On_sale에 1이 포함되지 않은 Pet_ID는 2뿐이므로 최종 결과는 아래와 같다. </p>
<ul>
<li>결과
<img src="https://velog.velcdn.com/images/hong_journey/post/d9020e7b-96dd-4109-8df9-5f6bba65f1ef/image.png" alt=""></li>
</ul>
</br>

<h1 id="5-마무리하며">5. 마무리하며</h1>
<p>요약하면 데이터를 배열로 저장하는 상황은 1)데이터를 순서대로 저장하는 것이 중요한 경우이거나, 2)반복 가능성이 있는 값들을 단일 행에 저장하기 위함이다. 특히 두 번째 이유는 데이터 무결성 개념과도 연결된다고 하니, DB 개념을 더 공부해야할 것 같다..
</br>
</br></p>
<h1 id="6-ref">6. ref</h1>
<p>참고한 자료</p>
<ul>
<li>Kaggle Advanced SQL <a href="https://www.kaggle.com/code/alexisbcook/nested-and-repeated-data">course</a></li>
</ul>
<p>참고한 서적</p>
<ul>
<li>빌리아파 락쉬마난, 조던 티가니 지음 / &lt;구글 빅쿼리 완벽 가이드&gt; / 변성윤, 장현희 옮김</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>