<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>DataRami</title>
    <link>https://datarami.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Thu, 30 Jul 2026 15:19:18 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Ramihan</managingEditor>
    <image>
      <title>DataRami</title>
      <url>https://tistory1.daumcdn.net/tistory/4024984/attach/449000e8d01844dd881ee835e3550ebb</url>
      <link>https://datarami.tistory.com</link>
    </image>
    <item>
      <title>BigQueryML로 유저 세그먼트 클러스터링하기</title>
      <link>https://datarami.tistory.com/39</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;BigQueryML?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글 빅쿼리 콘솔에서 SQL 쿼리로 간편하게 머신러닝 모델을 만들고, 실행할 수 있는 서비스입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적인 회귀 모델부터 K-means 클러스터링, 시계열 분석, DNN, 랜덤 포레스트, XGBoost 등 다양한 모델을 활용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;유저 세그먼트?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스를 이용하는 고객을 임의의 기준으로 분류한 결과를 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주로 User Persona라고 해서 하나의 모델로 유저를 정의하는 경우도 있지만, 사용자 집단의 동질성이 낮은 경우는 하나의 페르소나보다 각각의 군집으로 유저를 분류하는 것이 유리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 유저를 분류하는 것이 중요한 이유는 각 분류마다 원하는 것과 행동 양식이 다르기 때문에 만족도를 느끼는 지점 역시 다르고, 이는 곧 기능 개발과 캠페인 메시지 전달 방향을 설정하는 데도 큰 영향을 끼칠 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;본론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BigQueryML에서 유저 세그먼트를 클러스터링하는 방법은 매우 간단합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저들의 행동 패턴 몇 가지를 집계한 테이블을 Select하고, 이를 모델로 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시는 기본 제공 모델인 K-means 클러스터링을 사용했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1698361862533&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE OR REPLACE MODEL `mydataset.mymodel`
OPTIONS( 
	MODEL_TYPE='KMEANS',
  	KMEANS_INIT_METHOD='KMEANS++',
	NUM_CLUSTERS=8 
) AS

SELECT
  -- user_id,
  feature1,
  feature2,
  feature3,
  feature4
FROM `mydataset.myusertable`&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리에 대해 하나씩 설명하면, 첫 줄은 생성할 모델에 대한 타겟입니다. 생성된 모델은 여느 테이블처럼 빅쿼리의 데이터셋 안에 저장됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 옵션의 MODEL_TYPE에서 실행할 모델을 정하고, KMEANS 모델일 경우 KMEANS_INIT_METHOD 옵션을 추가로 지정할 수 있습니다. &lt;a href=&quot;https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-create-kmeans#kmeans_init_method&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;자세한 설명&lt;/a&gt;&amp;nbsp;참고 가능하며, 마지막 NUM_CLUSTERS는 최종적으로 분할할 클러스터의 갯수를 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리를 실행한 후 지정한 위치에 생성된 모델을 클릭해보면 '평가' 탭이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 클러스터의 각 인덱스마다 모델이 학습한 데이터가 어떻게 분포되어 있는지 직관적으로 확인하실 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1498&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wupql/btszeiTZbUJ/NWLvRlEORPtxVKMpX6au4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wupql/btszeiTZbUJ/NWLvRlEORPtxVKMpX6au4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wupql/btszeiTZbUJ/NWLvRlEORPtxVKMpX6au4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fwupql%2FbtszeiTZbUJ%2FNWLvRlEORPtxVKMpX6au4K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1498&quot; height=&quot;480&quot; data-origin-width=&quot;1498&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림에 대해 설명하면 왼쪽부터 피쳐 1,2,3,4번이고, 위쪽부터 클러스터 1,2,3,4,5,6,7,8번입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 셀마다 피쳐들이 어느 정도의 기준 값을 가지고 있는지 나타납니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 것은 지금부터입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 학습된 모델을 바탕으로 앞으로 유저들이 추가되거나 활동 양상이 변할 때 다시 세그멘테이션을 해 주어야 하는데요,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 방법 역시 간단합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빅쿼리 콘솔에서 아래와 같은 쿼리를 실행합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1698362466355&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT
  centroid_id, user_id
FROM
  ML.PREDICT(MODEL `mydataset.mymodel`,
    (
    SELECT
      user_id,
      feature1,
      feature2,
      feature3,
      feature4
    FROM
      `mydataset.myusertable`))&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때 주의해야 할 부분은 모델을 생성할 때는 user_id 필드가 없어야 하고,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델을 통해 할당할 때는 user_id 필드 + 나머지 피쳐도 동일하게 들어가야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 ML.PREDICT 명령어를 실행했을 때 리턴되는 필드는 더 다양하지만, 결과적으로 필요한 것은 어떤 유저가 어느 클러스터에 포함되었는가이기 때문에 최종 결과값에 centroid_id와 user_id 두 가지 필드만을 select했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빅쿼리 Dataform또는 DBT, Airflow 등 다양한 솔루션을 통해 이 작업을 자동화할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 매번 업데이트되는 유저 세그먼트를 어떻게 활용하느냐가 중요할 텐데요,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들자면 실험적인 피쳐를 런칭할 때 세그먼트 별로 목표를 설정해서 모니터링할 수도 있고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 실험군 자체를 세그먼트 별로 나눌 수도 있고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;광고 타깃을 뽑을 때 세그먼트를 직접 고려할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 이렇게 클러스터링을 할 때 유의할 점도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;퀄리티가 낮은 피쳐들을 사용하면 군집이 제대로 형성되지 않거나 군집 내의 동질성이 떨어질 수 있고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어느 정도는 직관을 이용해 숫자로 이루어진 세그먼트를 살아 숨쉬는 사람의 모습으로 상상할 수 있는 지혜도 필요합니다.&lt;/p&gt;</description>
      <category>About Data</category>
      <category>BigQueryML</category>
      <category>clustering</category>
      <category>Kmeans</category>
      <category>데이터분석</category>
      <category>빅쿼리</category>
      <category>유저 세그먼트</category>
      <category>클러스터링</category>
      <author>Ramihan</author>
      <guid isPermaLink="true">https://datarami.tistory.com/39</guid>
      <comments>https://datarami.tistory.com/39#entry39comment</comments>
      <pubDate>Fri, 27 Oct 2023 08:29:58 +0900</pubDate>
    </item>
    <item>
      <title>빅쿼리로 데이터 인프라 이전하기 (2) 사용성 최적화</title>
      <link>https://datarami.tistory.com/38</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서는 빅쿼리 ETL 비용 최적화에 대해 소개했었는데요,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 빅쿼리를 활용한 데이터 접근성 관리와 일반 사용자들의 사용량 관리에 대해 작성해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Phase 4. 데이터 접근성 높이기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Trino 환경에서 가장 제한적이었던 부분은 일 배치였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 DB를 긁는 부하를 줄이기 위해 사용량이 가장 낮은 시간에 한정해 ETL 파이프라인을 하루에 한 번 가동했기 때문이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빅쿼리로 이전하면서 세웠던 계획 중 하나가 바로 가능하면 스트리밍, 최소 1시간 단위까지 배치 빈도를 늘리자- 였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 아래와 같은 기능들을 사용하게 되었습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;DataStream
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Google Cloud SQL, AWS RDS 등의 DB를 스트리밍할 수 있는 매니지드 서비스입니다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;DB의 update log를 이용해 새로운 데이터만 추가하기 때문에 서비스 DB에 영향이 거의 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Dataflow
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MongoDB, Kafka, Google Pub/Sub 등을 Apache Beam 파이프라인을 통해 스트리밍할 수 있는 매니지드 서비스입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희가 DW 구성을 위해 입수해야 하는 주요 데이터는 아래와 같았습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;AWS RDS(MySQL Aurora)&lt;/li&gt;
&lt;li&gt;MongoDB&lt;/li&gt;
&lt;li&gt;Client Log(Segment)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 중에서 RDS는 상기한 DataSteam을 통해 손쉽게 스트리밍을 구현할 수 있었는데요,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단 아직까지 BigQuery로 직접 전송할 때 Partitioning을 지원하지 않아 다음과 같은 방법을 이용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DataStream -&amp;gt; Cloud Storage로 스트리밍 적재 -&amp;gt; Cloud Function에서 실시간으로 Storage path를 hive Partition 방식으로 변경 -&amp;gt; Bigquery에서 External Table 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 함으로써 실시간성을 유지하면서, 파티션 기능을 이용할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 MongoDB의 경우, 기존 데이터 마이그레이션과 스트리밍 파이프라인을 분리해서 이전 작업을 진행했는데요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마이그레이션은 Apache Beam 코드를 통해 MongoDB를 직접 읽어오는 방식으로 일괄 처리가 가능했고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스트리밍의 경우 MongoDB의 Change Stream을 활용했는데요, 이 부분은 Apache Beam에서 아직 Change Stream에 대한 메서드가 지원되지 않아 중간에 Kafka를 끼워 넣었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하여 Kafka에서 MongoDB Change Stream을 구독하고, Dataflow에서 Kafka를 스트리밍해서 BigQuery로 전송하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 Client Log의 경우, Segment라는 플랫폼을 이용해 다양한 destination으로 보내고 있었는데요,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 Google Pub/Sub destination을 추가하고, dataflow를 이용해 Pub/Sub으로 메시지가 들어올 때마다 BigQuery로 전송하도록 했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 해서 주요 데이터 원천에 대한 스트리밍 파이프라인을 완성했고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DW 수준에서는 이전 포스트에서 소개한 Dataform을 활용해 15분 ~ 한 시간 단위의 배치를 구현할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 실시간 KPI 리포팅과 시계열 데이터 쿼리의 일원화로 정합성을 개선하고 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;데이터에 기반한 당일 액션이 가능하게 되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2588&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u7oqt/btsjqxj7Za7/OqnNbuJ4uUJuwiXXizqQr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u7oqt/btsjqxj7Za7/OqnNbuJ4uUJuwiXXizqQr1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u7oqt/btsjqxj7Za7/OqnNbuJ4uUJuwiXXizqQr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu7oqt%2Fbtsjqxj7Za7%2FOqnNbuJ4uUJuwiXXizqQr1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2588&quot; height=&quot;400&quot; data-origin-width=&quot;2588&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Phase 5. 데이터 사용량 관리하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 비용 이야기로 돌아와,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기초 파이프라인 비용을 크게 절감했지만 실 사용 환경에서 내부 사용자들의 사용량이 최종 비용에는 더 큰 영향을 미칠 것이기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분에 대한 정책이 필요했습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;프로젝트 단위에서의 할당량 설정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Google Cloud 자체에서 빅쿼리 사용량에 대한 한도를 설정할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&amp;nbsp;BigQuery 콘솔에서의 할당량 설정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;콘솔을 이용할 때, 개별 쿼리에 대한 한도를 설정할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 저희는 내부 사용자 전체에 빅쿼리 콘솔을 개방하지는 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유 중 하나는 빅쿼리 콘솔에서 바로 시각화할 수 있는 기능이 약하기 때문에 보다 시각화에 초점을 맞춘 다른 BI툴로 통합된 쿼리 환경을 제공하고자 했기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희가 선택한 BI툴은 Metabase인데요,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 툴의 강점은 SQL을 사용하지 않고 대화형 GUI를 통해 쿼리를 조회할 수 있다는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하여 SQL을 다루지 못하는 사용자가 대부분인 환경에서 DA들의 단순 SQL 작성 리퀘스트를 줄일 수 있고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청을 어려워하는 사용자들 역시 스스로 간단한 데이터를 조회할 수 있기 때문에 데이터 접근성 역시 개선될 것이라는 판단이었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1532&quot; data-origin-height=&quot;1086&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUNH3l/btsjjAbU30D/5KFRsxqf8HbKH0YDqgBhyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUNH3l/btsjjAbU30D/5KFRsxqf8HbKH0YDqgBhyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUNH3l/btsjjAbU30D/5KFRsxqf8HbKH0YDqgBhyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUNH3l%2FbtsjjAbU30D%2F5KFRsxqf8HbKH0YDqgBhyK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1532&quot; height=&quot;1086&quot; data-origin-width=&quot;1532&quot; data-origin-height=&quot;1086&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이런 방식으로 데이터를 제공하기 위해서는 DW 구조화가 잘 되어 있어야만 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Raw Data를 그대로 옮겨놓는 방식으로는 DB 이해도가 낮은 사용자들이 활용하기 어렵기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이번 데이터 인프라 이전 과정에서 비용 다음으로 중점을 둔 것이 DW Architecture였고&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 데이터를 단순한 Select, 최대 한두 번의 Join 만으로 리포팅할 수 있도록 설계했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데, 3rd party BI 툴을 사용하면 개별 쿼리에 대한 사용량을 어떻게 관리할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 Metabase에는 아직 빅쿼리 사용량을 통제할 수 있는 기능은 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 대한 대안으로, 사용량에 대한 Alert을 도입했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Metabase에서 제공하는 query hashing 기능과 BigQuery api를 연계해서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저가 조회한 쿼리의 사용량을 측정하고, 특정 사이즈 이상일 때 슬랙으로 Alert을 보내는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 사후적이지만 규모가 큰 쿼리 또는 리포트를 관리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 최종 테이블 사이즈를 고려해서 metabase에는 특정 데이터셋만 연결하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 테이블들은 실수로 풀 스캔을 하더라도 비용에 크게 지장이 가지 않도록 설계했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덕분에 아직까지 일반 사용자들의 쿼리 작업으로 인해 할당량 한도가 발동한 적은 없습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Epilogue. 빅쿼리 이전의 효과?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 이전이 완전히 마무리된 것은 아니어서 잡음도 분명히 있고, 작업으로 인한 리소스 부하도 존재하지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 기존 환경 대비 빅쿼리 환경에서 데이터 파이프라인 운영에 대한 전체 비용이 50% 가까이 감소했고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DW 설계 개선의 영향으로 데이터 정합성과 리포트를 작성하는 DA들의 만족도 역시 높아질 것으로 기대하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 번에는 실제로 안정화가 되고 나서의 후기도 작성해 보겠습니다.&lt;/p&gt;</description>
      <category>etc.</category>
      <category>BI</category>
      <category>bigquery</category>
      <category>Data Engineer</category>
      <category>metabase</category>
      <category>데이터 엔지니어</category>
      <category>빅쿼리</category>
      <author>Ramihan</author>
      <guid isPermaLink="true">https://datarami.tistory.com/38</guid>
      <comments>https://datarami.tistory.com/38#entry38comment</comments>
      <pubDate>Sat, 10 Jun 2023 16:02:16 +0900</pubDate>
    </item>
    <item>
      <title>빅쿼리로 데이터 인프라 이전하기 (1) 비용 최적화 with Dataform</title>
      <link>https://datarami.tistory.com/37</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;결론부터 말씀드리면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;사용자 작업이 없는 주말 기준으로 데이터 파이프라인 운영 비용을 30만원대 -&amp;gt; 3만원대로 줄였습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;240&quot; data-origin-height=&quot;504&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvGs0I/btsjk6al1cI/BcAhAolyIcrHIqb2wVJb30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvGs0I/btsjk6al1cI/BcAhAolyIcrHIqb2wVJb30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvGs0I/btsjk6al1cI/BcAhAolyIcrHIqb2wVJb30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvGs0I%2Fbtsjk6al1cI%2FBcAhAolyIcrHIqb2wVJb30%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;240&quot; height=&quot;504&quot; data-origin-width=&quot;240&quot; data-origin-height=&quot;504&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Phase 0. Trino&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희는 기존에 AWS EKS를 활용해 스케일링이 가능한 Trino 환경을 사용하고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트 로그 데이터부터 MongoDB, RDS등의 데이터를 DW로 ETL하고, 전사 구성원들이 쿼리를 조회하는 용도로 활용되었죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점이 있다면 대용량의 데이터를 일별 스냅샷으로 적재해도 상대적으로 부담이 적다는 것과&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;누가 언제 어디서 어떻게 쿼리를 조회해도 비용 자체는 안정적으로 관리할 수 있다는 것이었죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나, 점차 데이터 사이즈와 쿼리량의 증가로 인해 서버 비용 외에 저장 비용도 누적되고&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;급격히 사용자가 몰리는 시점에는 처리 속도도 만족스럽지 못했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;또한, On-premise로 운영하다 보니 데이터 엔지니어들의 손이 가는 부분이 무척 많았죠.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Phase 1. 빅쿼리 종량제, 사실 부담돼요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대안 중에는 Google BigQuery도 있었지만, 선뜻 마음이 가지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용량에 따라 비용이 늘어나는 방식 때문에 비용 컨트롤도 어렵고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로 인해 누구나 자유롭게 데이터에 접근하기도 어려울 것 같다는 생각이 들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 빅쿼리 이전을 계획하면서, 아래와 같은 부분을 고려했습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;운영 리소스를 줄일 수 있는가&lt;/li&gt;
&lt;li&gt;ETL 파이프라인 운영 비용이 절감되는가&lt;/li&gt;
&lt;li&gt;데이터 접근성 저해 요소가 없는가&lt;/li&gt;
&lt;li&gt;일반 사용자의 사용량 컨트롤이 가능한가&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅의 목적은 위 네 가지 고민에 대해 제가 찾은 해결책을 공유하고자 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Phase 2. 운영 리소스 줄이기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Trino 환경 운영 당시, 데이터 엔지니어는 2명이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 명은 인프라, 한 명은 파이프라인 운영 중심으로 업무를 분담했지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 + 스토리지 + 메타스토어 + 에어플로우 + ETL 코드 + BI 툴 등을 모두 On-premise로 관리하기에는 빠듯했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 구글 클라우드로 데이터 환경을 이전하면서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BigQuery, Dataform, Composer 등의 매니지드 서비스를 통해 조금이나마 운영 부담이 줄어들 것을 기대했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 Cloud Build를 활용한 배포 자동화 시스템을 구축하고, ETL 코드 작업을 최소화하여&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환경 이전 과도기를 제외하고는 보다 수월하게 관리할 수 있음을 체감했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Phase 3. ETL 파이프라인 운영 비용 절감하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빅쿼리로 이전하면서 가장 신경을 많이 썼던 부분입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 환경보다 단 만원이라도 비용을 절감해야만 이전을 하는 의미가 있었기 때문에,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 파이프라인의 많은 부분을 새로 설계했습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;DW의 대부분을 증분 적재한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스냅샷으로 적재하는 이점도 분명 있지만, 실제로 사용자들이 조회하는 것은 대부분 가장 최신 상태의 데이터였고&lt;br /&gt;스냅샷 적재 시, 조회할 때 주의하지 않으면 전체 내역을 스캔하는 일도 발생할 수 있기 때문에 &lt;br /&gt;이를 방지하기 위해 업데이트 시점 기준으로 증분 적재를 하게 되었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Dataform을 활용한 증분 코드 작성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;BigQuery에 포함되어있는 Dataform 서비스를 이용해 SQL + 자바스크립트 기반의 ETL 코드를 작성했습니다.&lt;br /&gt;아래는 코드 Configuration 예시입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1316&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rPwt6/btsjkG33qme/xBD2hiTxc9M4kkrTuhM8Wk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rPwt6/btsjkG33qme/xBD2hiTxc9M4kkrTuhM8Wk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rPwt6/btsjkG33qme/xBD2hiTxc9M4kkrTuhM8Wk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrPwt6%2FbtsjkG33qme%2FxBD2hiTxc9M4kkrTuhM8Wk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;640&quot; height=&quot;498&quot; data-origin-width=&quot;1316&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL 쿼리 상단에 들어가는 이 설정이 Dataform 코드의 특징인데요, 하나씩 설명하자면&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;type : [table, incremental, view] 중 하나를 선택합니다.&amp;nbsp;&lt;br /&gt;table은 테이블 전체를 truncate하는 것이고, incremental은 upsert, view는 뷰 테이블을 작성합니다.&lt;/li&gt;
&lt;li&gt;schema : 테이블이 적재될 빅쿼리 데이터셋을 지정합니다.&lt;/li&gt;
&lt;li&gt;name : 적재할 테이블 이름을 지정합니다.&lt;/li&gt;
&lt;li&gt;bigquery : 빅쿼리 관련 설정을 지정합니다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;clusterBy : 빅쿼리 테이블의 클러스터링 필드를 지정합니다.&lt;/li&gt;
&lt;li&gt;partitionBy : 테이블 조회 비용에 가장 큰 영향을 끼치는 파티션 필드를 지정합니다.&lt;br /&gt;파티션을 지정한 후, 파티션 단위로 조회할 경우 해당 파티션에 대한 스캔 비용만 부과되기 때문입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;updatePartitionFilter&lt;/b&gt;: &lt;b&gt;incremental 기능을 이용할 때 가장 중요한 부분 &lt;/b&gt;중 하나입니다.&lt;br /&gt;upsert 방식을 구현하기 위해, temp 테이블을 작성한 후 기존 테이블에 merge하는데 이 때 파티션 필터가 설정되어 있지 않으면 기존 테이블을 full-scan하게 됩니다.&lt;br /&gt;실제로 일별 증분으로 처리다고 하면 직전 일자 파티션만 비교해도 되는 경우가 대다수이기 때문에 &lt;b&gt;이 구문을 활용해 비용을 크게 절감할 수 있었습니다.&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;tags : 해당 코드의 태그를 지정합니다. 태그 기준으로 복수의 코드를 동시 실행시킬 수 있기 때문에 &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;Composer와 연계해서 사용할 때&lt;span&gt;&amp;nbsp;유용합니다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;uniqueKey : incremental 기능을 이용할 때, 기준이 되는 고유 키 값입니다. 이 값으로 그루핑했을 때 고유한 row가 나오지 않으면 merge 단계에서 에러가 발생합니다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;disabled : 특정 조건이 충족되지 않으면 코드를 실행시키지 않는 기능입니다.&amp;nbsp;&lt;br /&gt;현재 저희가 설정해둔 것은 'executionDate'라는 variable 값이 없으면 코드를 실행시키지 않도록 했습니다.&lt;br /&gt;이렇게 한 까닭은, 증분 적재 시 Composer에서 logical date를 가져와 데이터 조회 일자 변수를 할당하는데 이 값이 없을 때 풀 스캔하는 것을 방지하기 위해서입니다.&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 외에도 pre_operations, post_operations 등 유용한 코드 블록들을 활용해 쿼리의 복잡도는 낮추고 자동화 효율은 높일 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 SQL + airflow를 통한 ETL 스케줄링에서 늘 신경쓰였던 부분이 테이블 간의 의존성 관리였는데요,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dataform에서는 &lt;b&gt;${ref(&quot;table path&quot;)}&lt;/b&gt; 구문을 이용해 종속 테이블이 변경되어도 의존성을 관리하기가 쉽고,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Backfill 코드를 따로 관리할 필요 없이 &lt;b&gt;${when(incremental())}&lt;/b&gt; 구문을 이용해 쿼리를 분기할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 BigQuery를 테스트하는 과정에서 위의 기능들을 제대로 활용하지 못해 추가로 발생한 비용이 서두에서 말씀드린 대로 10배에 달했으니, 꼭 참고하시기 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 이어지는 글에서 데이터 접근성과 사용량 관리에 대해 이야기해 보도록 하겠습니다.&lt;/p&gt;</description>
      <category>etc.</category>
      <category>bigquery</category>
      <category>Data Engineer</category>
      <category>Dataform</category>
      <category>ETL</category>
      <category>google cloud</category>
      <category>데이터 엔지니어</category>
      <category>데이터 파이프라인</category>
      <category>데이터폼</category>
      <category>빅쿼리</category>
      <category>빅쿼리 비용</category>
      <author>Ramihan</author>
      <guid isPermaLink="true">https://datarami.tistory.com/37</guid>
      <comments>https://datarami.tistory.com/37#entry37comment</comments>
      <pubDate>Sat, 10 Jun 2023 15:08:15 +0900</pubDate>
    </item>
    <item>
      <title>웹알못의 데이터 어드민 사이트 만들기(feat. Streamlit) 5. Epilogue</title>
      <link>https://datarami.tistory.com/36</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://datarami.tistory.com/31&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.09.10 - [etc.] - 웹알못의 데이터 어드민 사이트 만들기(feat. Streamlit) 0.Prologue&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1662744137195&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;웹알못의 데이터 어드민 사이트 만들기(feat. Streamlit) 0.Prologue&quot; data-og-description=&quot;스타트업의 데이터 팀에는 수많은 리퀘스트가 들어옵니다. 그 중에서 단순 추출과 개인정보 처리는 그 중요도와는 별개로, 요청은 많고 얻어가는 보람은 적은 일에 속하죠. 조직에 직접 인사이&quot; data-og-host=&quot;datarami.tistory.com&quot; data-og-source-url=&quot;https://datarami.tistory.com/31&quot; data-og-url=&quot;https://datarami.tistory.com/31&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/jYDZ1/hyPIMdVxub/mAwYuOYS9XPDcwo16g0rFK/img.png?width=800&amp;amp;height=788&amp;amp;face=0_0_800_788,https://scrap.kakaocdn.net/dn/GdCaR/hyPJYw3DgJ/ecBFrJnXmeNqh6UYkY4eDK/img.png?width=800&amp;amp;height=788&amp;amp;face=0_0_800_788,https://scrap.kakaocdn.net/dn/BRAoP/hyPIJIg3nF/UEssK0wAs5RQ0Kip15Z6gK/img.png?width=814&amp;amp;height=802&amp;amp;face=0_0_814_802&quot;&gt;&lt;a href=&quot;https://datarami.tistory.com/31&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://datarami.tistory.com/31&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/jYDZ1/hyPIMdVxub/mAwYuOYS9XPDcwo16g0rFK/img.png?width=800&amp;amp;height=788&amp;amp;face=0_0_800_788,https://scrap.kakaocdn.net/dn/GdCaR/hyPJYw3DgJ/ecBFrJnXmeNqh6UYkY4eDK/img.png?width=800&amp;amp;height=788&amp;amp;face=0_0_800_788,https://scrap.kakaocdn.net/dn/BRAoP/hyPIJIg3nF/UEssK0wAs5RQ0Kip15Z6gK/img.png?width=814&amp;amp;height=802&amp;amp;face=0_0_814_802');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;웹알못의 데이터 어드민 사이트 만들기(feat. Streamlit) 0.Prologue&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;스타트업의 데이터 팀에는 수많은 리퀘스트가 들어옵니다. 그 중에서 단순 추출과 개인정보 처리는 그 중요도와는 별개로, 요청은 많고 얻어가는 보람은 적은 일에 속하죠. 조직에 직접 인사이&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;datarami.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://datarami.tistory.com/32&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.09.10 - [etc.] - 웹알못의 데이터 어드민 사이트 만들기(feat. Streamlit) 1. 환경 구축&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1662744139899&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;웹알못의 데이터 어드민 사이트 만들기(feat. Streamlit) 1. 환경 구축&quot; data-og-description=&quot;Streamlit을 사용하기 위한 환경은 크게 어렵지 않습니다. pip install streamlit PIP 설치와 streamlit hello 실행 명령 한 줄이면 로컬에서도 웹앱을 띄울 수 있으니까요. 이 말은 즉, 파이썬과 외부 네트워크&quot; data-og-host=&quot;datarami.tistory.com&quot; data-og-source-url=&quot;https://datarami.tistory.com/32&quot; data-og-url=&quot;https://datarami.tistory.com/32&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bKByLd/hyPJZpase8/KQHECLBrD62VBKOOQJfUN0/img.png?width=800&amp;amp;height=446&amp;amp;face=0_0_800_446,https://scrap.kakaocdn.net/dn/hBclR/hyPJS4EYAh/GhgjaCJu31YensMUe8U1kk/img.png?width=800&amp;amp;height=446&amp;amp;face=0_0_800_446,https://scrap.kakaocdn.net/dn/HLYdD/hyPJYcJIMw/hJy4LHB4rqhekiRUKkCvz1/img.png?width=1204&amp;amp;height=672&amp;amp;face=0_0_1204_672&quot;&gt;&lt;a href=&quot;https://datarami.tistory.com/32&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://datarami.tistory.com/32&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bKByLd/hyPJZpase8/KQHECLBrD62VBKOOQJfUN0/img.png?width=800&amp;amp;height=446&amp;amp;face=0_0_800_446,https://scrap.kakaocdn.net/dn/hBclR/hyPJS4EYAh/GhgjaCJu31YensMUe8U1kk/img.png?width=800&amp;amp;height=446&amp;amp;face=0_0_800_446,https://scrap.kakaocdn.net/dn/HLYdD/hyPJYcJIMw/hJy4LHB4rqhekiRUKkCvz1/img.png?width=1204&amp;amp;height=672&amp;amp;face=0_0_1204_672');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;웹알못의 데이터 어드민 사이트 만들기(feat. Streamlit) 1. 환경 구축&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Streamlit을 사용하기 위한 환경은 크게 어렵지 않습니다. pip install streamlit PIP 설치와 streamlit hello 실행 명령 한 줄이면 로컬에서도 웹앱을 띄울 수 있으니까요. 이 말은 즉, 파이썬과 외부 네트워크&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;datarami.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://datarami.tistory.com/33&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.09.10 - [etc.] - 웹알못의 데이터 어드민 사이트 만들기(feat. Streamlit) 2. 페이지 작성&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1662744146336&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;웹알못의 데이터 어드민 사이트 만들기(feat. Streamlit) 2. 페이지 작성&quot; data-og-description=&quot;Streamlit을 띄운 목적이 데이터 애드혹을 줄이는 것이었기 때문에, 주요 테이블을 조회하는 간단한 쿼리를 누구나 클릭 베이스로 작성할 수 있는 페이지를 가장 먼저 작성해 봤습니다. 부끄럽지&quot; data-og-host=&quot;datarami.tistory.com&quot; data-og-source-url=&quot;https://datarami.tistory.com/33&quot; data-og-url=&quot;https://datarami.tistory.com/33&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/GJSkk/hyPJ7Oj7gr/gYNMUqDYeZ0x89OxOjeFsk/img.png?width=650&amp;amp;height=880&amp;amp;face=0_0_650_880,https://scrap.kakaocdn.net/dn/j7uvm/hyPJ2ztw8M/FuHKylOhyT4Leeln3ND0WK/img.png?width=650&amp;amp;height=880&amp;amp;face=0_0_650_880,https://scrap.kakaocdn.net/dn/b3k4RS/hyPJ4D2n2m/oRdZsHRJzoMjAUhMQr1cjk/img.png?width=650&amp;amp;height=880&amp;amp;face=0_0_650_880&quot;&gt;&lt;a href=&quot;https://datarami.tistory.com/33&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://datarami.tistory.com/33&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/GJSkk/hyPJ7Oj7gr/gYNMUqDYeZ0x89OxOjeFsk/img.png?width=650&amp;amp;height=880&amp;amp;face=0_0_650_880,https://scrap.kakaocdn.net/dn/j7uvm/hyPJ2ztw8M/FuHKylOhyT4Leeln3ND0WK/img.png?width=650&amp;amp;height=880&amp;amp;face=0_0_650_880,https://scrap.kakaocdn.net/dn/b3k4RS/hyPJ4D2n2m/oRdZsHRJzoMjAUhMQr1cjk/img.png?width=650&amp;amp;height=880&amp;amp;face=0_0_650_880');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;웹알못의 데이터 어드민 사이트 만들기(feat. Streamlit) 2. 페이지 작성&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Streamlit을 띄운 목적이 데이터 애드혹을 줄이는 것이었기 때문에, 주요 테이블을 조회하는 간단한 쿼리를 누구나 클릭 베이스로 작성할 수 있는 페이지를 가장 먼저 작성해 봤습니다. 부끄럽지&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;datarami.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://datarami.tistory.com/34&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.09.10 - [etc.] - 웹알못의 데이터 어드민 사이트 만들기(feat. Streamlit) 3. 오픈은 실전&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1662744161957&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;웹알못의 데이터 어드민 사이트 만들기(feat. Streamlit) 3. 오픈은 실전&quot; data-og-description=&quot;두 번째로 만든 서비스는 'userSelector'였습니다. 이번에도 다짜고짜 코드 먼저 첨부합니다. 더보기 import streamlit as st import pandas as pd from datetime import datetime, timedelta st.set_page_config(..&quot; data-og-host=&quot;datarami.tistory.com&quot; data-og-source-url=&quot;https://datarami.tistory.com/34&quot; data-og-url=&quot;https://datarami.tistory.com/34&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b0HjtG/hyPJYKBfRF/hYEdDyZRoLH4dmpK0QFz50/img.png?width=754&amp;amp;height=813&amp;amp;face=0_0_754_813,https://scrap.kakaocdn.net/dn/cZBJET/hyPJZpbUjb/KkkQuGAqKhDe7bmKl654BK/img.png?width=754&amp;amp;height=813&amp;amp;face=0_0_754_813,https://scrap.kakaocdn.net/dn/bRd28i/hyPJ8zFyy2/7UOQ7C8HKfxrI8GS0Tg8H0/img.png?width=754&amp;amp;height=813&amp;amp;face=0_0_754_813&quot;&gt;&lt;a href=&quot;https://datarami.tistory.com/34&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://datarami.tistory.com/34&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b0HjtG/hyPJYKBfRF/hYEdDyZRoLH4dmpK0QFz50/img.png?width=754&amp;amp;height=813&amp;amp;face=0_0_754_813,https://scrap.kakaocdn.net/dn/cZBJET/hyPJZpbUjb/KkkQuGAqKhDe7bmKl654BK/img.png?width=754&amp;amp;height=813&amp;amp;face=0_0_754_813,https://scrap.kakaocdn.net/dn/bRd28i/hyPJ8zFyy2/7UOQ7C8HKfxrI8GS0Tg8H0/img.png?width=754&amp;amp;height=813&amp;amp;face=0_0_754_813');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;웹알못의 데이터 어드민 사이트 만들기(feat. Streamlit) 3. 오픈은 실전&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;두 번째로 만든 서비스는 'userSelector'였습니다. 이번에도 다짜고짜 코드 먼저 첨부합니다. 더보기 import streamlit as st import pandas as pd from datetime import datetime, timedelta st.set_page_config(..&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;datarami.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://datarami.tistory.com/35&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.09.10 - [etc.] - 웹알못의 데이터 어드민 사이트 만들기(feat. Streamlit) 4. 보안을 보완&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1662744168417&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;웹알못의 데이터 어드민 사이트 만들기(feat. Streamlit) 4. 보안을 보완&quot; data-og-description=&quot;데이터 보안에 대한 피드백을 받은 후 바로 보안 팀과 논의에 들어갑니다. 보완해야 할 부분은 두 가지였습니다. 1. 허용된 구성원만 접근할 수 있게 할 것 2. 데이터 추출 시 기록을 남길 것 다른&quot; data-og-host=&quot;datarami.tistory.com&quot; data-og-source-url=&quot;https://datarami.tistory.com/35&quot; data-og-url=&quot;https://datarami.tistory.com/35&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/inJtm/hyPJUal6SM/2y9YrftLtKEM1Bd97d70g0/img.png?width=731&amp;amp;height=628&amp;amp;face=0_0_731_628,https://scrap.kakaocdn.net/dn/eD2xf/hyPJ5iEGrh/mq8lS9A3EbCjMkU4TsmKH0/img.png?width=731&amp;amp;height=628&amp;amp;face=0_0_731_628,https://scrap.kakaocdn.net/dn/beBPEA/hyPIEAb8fn/wuMbsjpwQjaOAuYzsEN5jK/img.png?width=731&amp;amp;height=628&amp;amp;face=0_0_731_628&quot;&gt;&lt;a href=&quot;https://datarami.tistory.com/35&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://datarami.tistory.com/35&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/inJtm/hyPJUal6SM/2y9YrftLtKEM1Bd97d70g0/img.png?width=731&amp;amp;height=628&amp;amp;face=0_0_731_628,https://scrap.kakaocdn.net/dn/eD2xf/hyPJ5iEGrh/mq8lS9A3EbCjMkU4TsmKH0/img.png?width=731&amp;amp;height=628&amp;amp;face=0_0_731_628,https://scrap.kakaocdn.net/dn/beBPEA/hyPIEAb8fn/wuMbsjpwQjaOAuYzsEN5jK/img.png?width=731&amp;amp;height=628&amp;amp;face=0_0_731_628');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;웹알못의 데이터 어드민 사이트 만들기(feat. Streamlit) 4. 보안을 보완&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;데이터 보안에 대한 피드백을 받은 후 바로 보안 팀과 논의에 들어갑니다. 보완해야 할 부분은 두 가지였습니다. 1. 허용된 구성원만 접근할 수 있게 할 것 2. 데이터 추출 시 기록을 남길 것 다른&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;datarami.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우여곡절을 거치면서 '데이터 어드민'이라고 명명한 사이트를 만들긴 했지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라이브러리의 힘을 빌려 어설프게 만들었다보니 사용성이나 성능 면에서 아쉬운 점도 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 에필로그는 앞으로의 로드맵에 대해서 얘기해보고 싶은데요,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 어드민 사이트의 피드백 페이지에는 아래와 같은 아이디어들이 정리되어 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;829&quot; data-origin-height=&quot;808&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2fCBz/btrLNpTTUch/Uv11WrEINvulrZnKdUExHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2fCBz/btrLNpTTUch/Uv11WrEINvulrZnKdUExHk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2fCBz/btrLNpTTUch/Uv11WrEINvulrZnKdUExHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2fCBz%2FbtrLNpTTUch%2FUv11WrEINvulrZnKdUExHk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;829&quot; height=&quot;808&quot; data-origin-width=&quot;829&quot; data-origin-height=&quot;808&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 중에서 구현된 것도 있고, 구현할 것도 있고, 구현하지 않을 것도 있겠지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 건 이런 것들을 만들 수 있는 가능성이 있다는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 페이지를 통해 저의 아이디어 뿐만 아니라 실제로 데이터에 접근해야 하는 실무자들의 아이디어를 받고 싶고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔지니어링 측면에서는 웹 프레임워크를 차차 공부해서 내재화하는 것이 목표이기도 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 만들어낸 서비스가 단지 개인의 만족이 아니라&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 구성원들에게 도움이 되고, 이를 통해 우리 데이터 팀의 성장에도 도움이 되기를 바랍니다.&lt;/p&gt;</description>
      <category>etc.</category>
      <author>Ramihan</author>
      <guid isPermaLink="true">https://datarami.tistory.com/36</guid>
      <comments>https://datarami.tistory.com/36#entry36comment</comments>
      <pubDate>Sat, 10 Sep 2022 02:30:01 +0900</pubDate>
    </item>
    <item>
      <title>웹알못의 데이터 어드민 사이트 만들기(feat. Streamlit) 4. 보안을 보완</title>
      <link>https://datarami.tistory.com/35</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 보안에 대한 피드백을 받은 후 바로 보안 팀과 논의에 들어갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보완해야 할 부분은 두 가지였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 허용된 구성원만 접근할 수 있게 할 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 데이터 추출 시 기록을 남길 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 오픈소스나 SaaS의 경우 몇 가지 설정을 통해 구글 oauth 로그인을 추가할 수 있었기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에도 그냥 추가하면 되겠거니 했지만 Streamlit은 호락호락하지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flask도, django도 아닌 tornado 프레임워크 기반인데다,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라이브러리를 건드리지 않고 페이지 코드에서 로그인을 추가할 수 없었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐면 정해진 방법 외에는 리다이렉트 페이지를 생성할 수 있는 방법이 없었기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;...는 사실 등잔 밑이 어두운 웹알못이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Streamlit 공식 문서에서 친절하게도 k8s 기반으로 배포할 때 인증에 대해 알려주고 있었기 때문이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.streamlit.io/knowledge-base/tutorials/deploy/kubernetes#configure-a-google-oauth-client-and-oauth2-proxy&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.streamlit.io/knowledge-base/tutorials/deploy/kubernetes#configure-a-google-oauth-client-and-oauth2-proxy&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1662741842612&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Streamlit Docs&quot; data-og-description=&quot;Join the community Streamlit is more than just a way to make data apps, it's also a community of creators that share their apps and ideas and help each other make their work better. Please come join us on the community forum. We love to hear your questions&quot; data-og-host=&quot;docs.streamlit.io&quot; data-og-source-url=&quot;https://docs.streamlit.io/knowledge-base/tutorials/deploy/kubernetes#configure-a-google-oauth-client-and-oauth2-proxy&quot; data-og-url=&quot;https://docs.streamlit.io/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/okYR8/hyPJZvYqnj/JA0mQ88ItrriXOfikkOj41/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/vxZ02/hyPJSwPsI0/yqfsHnmolQPzIKGax1GaK1/img.jpg?width=280&amp;amp;height=150&amp;amp;face=0_0_280_150,https://scrap.kakaocdn.net/dn/7ptJW/hyPJ0IpJS4/md7PpaK0ncNRUKrp8mHSj0/img.png?width=362&amp;amp;height=313&amp;amp;face=0_0_362_313&quot;&gt;&lt;a href=&quot;https://docs.streamlit.io/knowledge-base/tutorials/deploy/kubernetes#configure-a-google-oauth-client-and-oauth2-proxy&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.streamlit.io/knowledge-base/tutorials/deploy/kubernetes#configure-a-google-oauth-client-and-oauth2-proxy&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/okYR8/hyPJZvYqnj/JA0mQ88ItrriXOfikkOj41/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/vxZ02/hyPJSwPsI0/yqfsHnmolQPzIKGax1GaK1/img.jpg?width=280&amp;amp;height=150&amp;amp;face=0_0_280_150,https://scrap.kakaocdn.net/dn/7ptJW/hyPJ0IpJS4/md7PpaK0ncNRUKrp8mHSj0/img.png?width=362&amp;amp;height=313&amp;amp;face=0_0_362_313');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Streamlit Docs&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Join the community Streamlit is more than just a way to make data apps, it's also a community of creators that share their apps and ideas and help each other make their work better. Please come join us on the community forum. We love to hear your questions&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.streamlit.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 공식 문서를 더 잘 살펴보자는 반성을 하며&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;oauth2-proxy 사이드카 컨테이너를 통해 인증 과정을 추가하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://oauth2-proxy.github.io/oauth2-proxy/docs/configuration/oauth_provider#google-auth-provider&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://oauth2-proxy.github.io/oauth2-proxy/docs/configuration/oauth_provider#google-auth-provider&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1662742124214&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;OAuth Provider Configuration | OAuth2 Proxy&quot; data-og-description=&quot;You will need to register an OAuth application with a Provider (Google, GitHub or another provider), and configure it with Redirect URI(s) for the domain you intend to run oauth2-proxy on.&quot; data-og-host=&quot;oauth2-proxy.github.io&quot; data-og-source-url=&quot;https://oauth2-proxy.github.io/oauth2-proxy/docs/configuration/oauth_provider#google-auth-provider&quot; data-og-url=&quot;https://oauth2-proxy.github.io/oauth2-proxy/docs/configuration/oauth_provider&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://oauth2-proxy.github.io/oauth2-proxy/docs/configuration/oauth_provider#google-auth-provider&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://oauth2-proxy.github.io/oauth2-proxy/docs/configuration/oauth_provider#google-auth-provider&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;OAuth Provider Configuration | OAuth2 Proxy&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;You will need to register an OAuth application with a Provider (Google, GitHub or another provider), and configure it with Redirect URI(s) for the domain you intend to run oauth2-proxy on.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;oauth2-proxy.github.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 조직의 모든 구성원이 아닌 허용된 사용자만 접근할 수 있도록&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GCP 콘솔에서 그룹과 Service Account를 생성하고 연동했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 다시 오픈해도 되겠다 생각한 순간,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인간의 욕심은 끝이 없다고.. 이왕 보안을 도입한거 개인정보 처리 시스템까지 만들어버릴까? 의욕이 샘솟아 버립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롤로그에서 이야기한 것처럼 데이터 애드혹 중 큰 비중을 차지하는 것이 바로 개인정보 요청이기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 아래 로직대로 페이지를 구성해 봤습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;731&quot; data-origin-height=&quot;628&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vF7AS/btrLMABneXK/pqpOUQZeIHGus4yc8dC14K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vF7AS/btrLMABneXK/pqpOUQZeIHGus4yc8dC14K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vF7AS/btrLMABneXK/pqpOUQZeIHGus4yc8dC14K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvF7AS%2FbtrLMABneXK%2FpqpOUQZeIHGus4yc8dC14K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;731&quot; height=&quot;628&quot; data-origin-width=&quot;731&quot; data-origin-height=&quot;628&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;748&quot; data-origin-height=&quot;431&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZQsb0/btrLMx5E8ia/EoJnnzVHFzlzcYZ6TBIEJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZQsb0/btrLMx5E8ia/EoJnnzVHFzlzcYZ6TBIEJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZQsb0/btrLMx5E8ia/EoJnnzVHFzlzcYZ6TBIEJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZQsb0%2FbtrLMx5E8ia%2FEoJnnzVHFzlzcYZ6TBIEJ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;748&quot; height=&quot;431&quot; data-origin-width=&quot;748&quot; data-origin-height=&quot;431&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 허용된 사용자가 로그인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 개인정보 열람 신청 페이지에서 신청서를 작성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; - 신청자는 구글 로그인한 본인으로 고정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; - 신청 목적, 파기 일정 입력&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; - 개인정보 활용 및 파기 동의 체크&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 신청서가 제출되면 해당 건에 대한 신청 uid가 발급된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 개인정보 관리자가 별도의 페이지를 통해 uid를 입력하고 신청서를 승인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; - uid는 승인 후 한 시간 동안 유효하며, 다른 사용자가 사용할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 신청자가 개인정보 추출 페이지를 방문해 승인받은 uid를 입력하고 user id 리스트 파일을 업로드한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. 원하는 개인정보 필드를 선택하고 최종 파일을 다운로드한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용 기록을 남기는 구간은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 신청서 작성 시 : REQUESTED&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 신청서 승인 시 : CONFIRMED&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; 2-1. 신청서 반려 시 : DENIED&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 파일 다운로드 시 : DOWNLOADED&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용 기록은 아래와 같이 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 내역&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 신청자 정보&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 신청 uid&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 신청 시각&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 신청 목적&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 파기 일정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 동의 여부&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 승인 담당자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 승인 시각&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 다운로드 시각&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 다운로드한 정보 목록&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 상세 내역&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 신청 uid&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 다운로드 시각&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 다운로드한 user id 목록&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 슬랙을 연동해 개인정보 관리자들이 현황을 바로 파악할 수 있게 구성했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;380&quot; data-origin-height=&quot;97&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tTXA8/btrLLxZousS/wc7k3hhnZ6ykb6IvhWuLv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tTXA8/btrLLxZousS/wc7k3hhnZ6ykb6IvhWuLv0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tTXA8/btrLLxZousS/wc7k3hhnZ6ykb6IvhWuLv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtTXA8%2FbtrLLxZousS%2Fwc7k3hhnZ6ykb6IvhWuLv0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;380&quot; height=&quot;97&quot; data-origin-width=&quot;380&quot; data-origin-height=&quot;97&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다운로드되는 파일 역시 암호화된 zip 형식으로 보안을 강화했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1662743440892&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def encrypt_zip(df, password):
    csv = df.to_csv(f&quot;privacy_data.csv&quot;, index=False, encoding='utf-8-sig')
    pyminizip.compress(f&quot;privacy_data.csv&quot;, None, f&quot;privacy_data.zip&quot;, password, 1)
    os.remove(f&quot;privacy_data.csv&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보완 후 한 달만에 정식으로 서비스를 오픈하게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;358&quot; data-origin-height=&quot;111&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blYhQc/btrLMAg4H7c/zLClH6Qkndj6vRE0XK6zpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blYhQc/btrLMAg4H7c/zLClH6Qkndj6vRE0XK6zpK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blYhQc/btrLMAg4H7c/zLClH6Qkndj6vRE0XK6zpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblYhQc%2FbtrLMAg4H7c%2FzLClH6Qkndj6vRE0XK6zpK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;358&quot; height=&quot;111&quot; data-origin-width=&quot;358&quot; data-origin-height=&quot;111&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 과제는 이제 어떻게 구성원들이 이 서비스를 사용하게 할 것인가..? 가 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 접근 자체에 허가가 필요하다 보니 선뜻 먼저 써보려는 분들이 많지 않고 이대로는 유령 서비스가 되겠다 싶어&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가이드 문서도 작성하고 주변을 통해 직접 알리기도 하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매일 개인정보를 다루어야 하는 물류, CRM 등 담당자들에게 우선적으로 계정을 부여하기도 하는 등&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실상 서비스 회사 안의 또다른 서비스를 만든 느낌입니다(?)&lt;/p&gt;</description>
      <category>etc.</category>
      <author>Ramihan</author>
      <guid isPermaLink="true">https://datarami.tistory.com/35</guid>
      <comments>https://datarami.tistory.com/35#entry35comment</comments>
      <pubDate>Sat, 10 Sep 2022 02:19:20 +0900</pubDate>
    </item>
    <item>
      <title>웹알못의 데이터 어드민 사이트 만들기(feat. Streamlit) 3. 오픈은 실전</title>
      <link>https://datarami.tistory.com/34</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째로 만든 서비스는 'userSelector'였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에도 다짜고짜 코드 먼저 첨부합니다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1662740103544&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import streamlit as st
import pandas as pd
from datetime import datetime, timedelta

st.set_page_config(page_title=&quot;userSelector&quot;, page_icon=&quot; &quot;, layout='wide')
st.title(&quot;dat-a-dmin [userSelector]&quot;)

###

@st.cache(ttl=10800)
def get_categories():
    engine = db.ps_engine()
    categories = pd.read_sql(&quot;select &quot;, engine)['category']
    return categories

@st.cache(ttl=10800)
def get_events():
    engine = db.ps_engine()
    events = pd.read_sql(&quot;select &quot;, engine)['event']
    return events

@st.cache(ttl=10800)
def get_pagename():
    engine = db.ps_engine()
    pagename = pd.read_sql(&quot;select &quot;, engine)['_name']
    return pagename

userTable = ''
logTable = ''
orderTable = ''
wishTable = ''
subscriptionTable = ''

d0_option = [
    '가입 시점',
    '최종 로그인 시점',
    'PV 횟수',
    '구매 횟수',
    '유입 경로'
    #...
]

depth0 = st.multiselect(
    '어떤 조건으로 유저를 추출할까요? (복수 선택)',
    d0_option
)

where_sentence = &quot;\nwhere 1=1&quot;
having_sentence='\ngroup by 1\nhaving 1=1'

signed_query=''
login_query=''
pv_query=''
order_query=''
utm_query=''
event_query=''
signed_join=''
login_join=''
pv_join=''
order_join=''
utm_join=''
event_join=''
extra_where='where 1=1'

for d0 in depth0:
    if '가입 시점' in d0:
        signed_where=''
        signed_at = st.date_input(
            '가입 기간을 선택하세요',
            [datetime.date(datetime.now() - timedelta(days=30)), datetime.date(datetime.now())]
        )
        signed_start = datetime.strftime(signed_at[0], '%Y-%m-%d')
        signed_end = datetime.strftime(signed_at[1], '%Y-%m-%d')
        signed_where += f&quot;  \nand date(created_at_kst) between date'{signed_start}' and date'{signed_end}'&quot;

        signed_query=f&quot;&quot;&quot;, signed as (
    select *
    from {userTable}
    {where_sentence}
    {signed_where}  
)      
        &quot;&quot;&quot;
        signed_join = &quot;join signed s on u.user_id = s.user_id&quot;
    
    if '최종 로그인 시점' in d0:
        login_where=''
        login_at = st.date_input(
            '최종 로그인 기간을 선택하세요',
            [datetime.date(datetime.now() - timedelta(days=30)), datetime.date(datetime.now())] 
        )
        login_start = datetime.strftime(login_at[0], '%Y-%m-%d')
        login_end = datetime.strftime(login_at[1], '%Y-%m-%d')
        login_where += f&quot;   \nand date(created_at_kst) between date'{login_start}' and date'{login_end}'&quot;

        login_query=f&quot;&quot;&quot;, login as (
    select *
    from {userTable}
    {where_sentence}
    {login_where}  
)      
        &quot;&quot;&quot;
        login_join = &quot;join login l on u.user_id = l.user_id&quot;
    
    if 'PV 횟수' in d0 or '구매 횟수' in d0 :
        log_where=''
        order_where=''
        wish_where=''
        log_having=''
        order_having=''
        wish_having=''

        d1_selector = st.selectbox(
            f'{d0} 기준을 선택하세요',
            ('전체', '특정 카테고리(depth 1)', '특정 상품')
        )

        date_selector = st.date_input(
            f'{d0} 기간을 선택하세요',
            [datetime.date(datetime.now() - timedelta(days=30)), datetime.date(datetime.now())] 
        )
        at_start = datetime.strftime(date_selector[0], '%Y-%m-%d')
        at_end = datetime.strftime(date_selector[1], '%Y-%m-%d')
        pymd_start = datetime.strftime(date_selector[0], '%Y%m%d')
        pymd_end = datetime.strftime(date_selector[1], '%Y%m%d')

        if 'PV' in d0:
            log_where += f&quot; \nand event = 'Product Viewed'\nand date(timestamp_kst) between date'{at_start}' and date'{at_end}'\nand p_ymd between '{pymd_start}' and '{pymd_end}'&quot;
        if '구매' in d0:
            order_where += f&quot;   \nand date(created_at_kst) between date'{at_start}' and date'{at_end}'&quot; 
       
        if d1_selector == '전체':
            pass
            
        if d1_selector == '특정 카테고리(depth 1)':
            categories = get_categories()
            category_selector = st.multiselect(
                f'{d0} 기준 카테고리준 선택하세요 (복수 선택)',
                categories
            )
            cate_list = []
            for c in category_selector:
                cate_list.append(&quot;'&quot;+c+&quot;'&quot;)
            category = ', '.join(cate_list)
            if 'PV' in d0:
                log_where += f&quot; \nand json_extract_scalar(properties, '$.main_category') in ({category})&quot;
            if '구매' in d0:
                order_where += f&quot;   \nand depth1 in ({category})&quot; 
            
        if d1_selector == '특정 상품':
            text_selector= st.text_input(
                f'{d0} 기준 상품 검색어를 입력하세요'
            )
            if 'PV' in d0:
                log_where += f&quot; \nand json_extract_scalar(properties, '$.product_title') like '%%{text_selector}%%'&quot;
            if '구매' in d0:
                order_where += f&quot;   \nand product_title like '%%{text_selector}%%'&quot;
                   
        score_selector = st.slider(
            f'{d0} 기준을 선택하세요',
            0, 200, (1, 10)
        )
        if score_selector == (0,0):
            if 'PV' in d0:
                pv_query=f&quot;&quot;&quot;, pv as (
        select user_id, count(id) cnt
        from {logTable}
        {where_sentence}
        {log_where}
        {having_sentence}    
    )   
                &quot;&quot;&quot;
                pv_join = &quot;left join pv p on u.user_id = p.user_id&quot;
                extra_where += &quot;\nand p.user_id is null&quot;

            if '구매' in d0:
                order_query=f&quot;&quot;&quot;, orders as (
        select user_f_id, count(distinct order_id) cnt
        from {orderTable}
        {where_sentence}
        {order_where}
        {having_sentence}    
    )   
                &quot;&quot;&quot;
                order_join = &quot;left join orders o on u.user_id = o.user_id&quot;
                extra_where += &quot;\nand o.user_id is null&quot;

        else:
            if 'PV' in d0:
                log_having += f&quot;    \nand count(id) between {score_selector[0]} and {score_selector[1]}&quot;
                pv_query=f&quot;&quot;&quot;, pv as (
        select user_id, count(id) cnt
        from {logTable}
        {where_sentence}
        {log_where}
        {having_sentence}    
        {log_having} 
    )   
                &quot;&quot;&quot;
                pv_join = &quot;join pv p on u.user_id = p.user_id&quot;

            if '구매' in d0:
                order_having += f&quot;  \nand count(distinct order_id) between {score_selector[0]} and {score_selector[1]}&quot;
                order_query=f&quot;&quot;&quot;, orders as (
        select user_f_id, count(distinct order_id) cnt
        from {orderTable}
        {where_sentence}
        {order_where}
        {having_sentence}    
        {order_having} 
    )   
                &quot;&quot;&quot;
                order_join = &quot;join orders o on u.user_id = o.user_id&quot;

    if '유입 경로' in d0:
        utm_where=''
        d1_selector = st.multiselect(
            '검색할 항목을 선택하세요 (복수 선택)',
            ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term']
        )
        utm_at = st.date_input(
            f'{d0} 기간을 선택하세요',
            [datetime.date(datetime.now() - timedelta(days=30)), datetime.date(datetime.now())]  
        )
        at_start = datetime.strftime(utm_at[0], '%Y-%m-%d')
        at_end = datetime.strftime(utm_at[1], '%Y-%m-%d')
        pymd_start = datetime.strftime(utm_at[0], '%Y%m%d')
        pymd_end = datetime.strftime(utm_at[1], '%Y%m%d')
        
        utm_where += f&quot; \nand p_ymd between '{pymd_start}' and '{pymd_end}' and date(timestamp_kst) between date'{at_start}' and date'{at_end}'&quot;

        for utm_d1 in d1_selector:
            d2_input = st.text_input(
                f'{utm_d1} 검색어를 입력하세요'  
            )
            utm_where += f&quot; \nand {utm_d1} like '%%{d2_input}%%'&quot;

        utm_query = f&quot;&quot;&quot;, utm as (
    select *
    from {logTable}
    {where_sentence}
    {utm_where}
)
        &quot;&quot;&quot;
        utm_join = &quot;join utm t on u.user_id = t.user_id&quot;

    if '특정 로그 경험' in d0:
        event_where=''
        event_at = st.date_input(
            f'{d0} 기간을 선택하세요',
            [datetime.date(datetime.now() - timedelta(days=30)), datetime.date(datetime.now())]  
        )
        at_start = datetime.strftime(event_at[0], '%Y-%m-%d')
        at_end = datetime.strftime(event_at[1], '%Y-%m-%d')
        pymd_start = datetime.strftime(event_at[0], '%Y%m%d')
        pymd_end = datetime.strftime(event_at[1], '%Y%m%d')
        
        event_where += f&quot;   \nand p_ymd between '{pymd_start}' and '{pymd_end}' and date(timestamp_kst) between date'{at_start}' and date'{at_end}'&quot;

        event_type = {
            'Page/Screen Viewed':&quot;\&quot;type\&quot; in ('page', 'screen')&quot;,
            'Track Event':&quot;\&quot;type\&quot; in ('track')&quot;,
            'Identify':&quot;\&quot;type\&quot; in ('identify')&quot;
        }
        d1_selector = st.multiselect(
            '검색할 로그의 유형을 선택하세요 (복수 선택)',
            ['Page/Screen Viewed', 'Track Event', 'Identify']
        ) 
        for d1_type in d1_selector:
            event_where += f'\nand {event_type.get(d1_type)}'

            if 'Page/Screen' in d1_type:
                page_selector = st.multiselect(
                    f'{d1_type} 로그를 선택하세요 (복수 선택)',
                    get_pagename()
                )
                page_list = []
                for p in page_selector:
                    page_list.append(&quot;'&quot;+p+&quot;'&quot;)
                event_where += f&quot;\nand event in ({', '.join(page_list)})&quot;

            if 'Track Event' in d1_type:
                event_selector = st.multiselect(
                    f'{d1_type} 로그를 선택하세요 (복수 선택)',
                    get_events()
                )
                event_list = []
                for e in event_selector:
                    event_list.append(&quot;'&quot;+e+&quot;'&quot;)
                event_where += f&quot;\nand event in ({', '.join(event_list)})&quot;    

        event_query = f&quot;&quot;&quot;, event as (
    select *
    from {logTable}
    {where_sentence}
    {event_where}
)
        &quot;&quot;&quot;
        event_join = &quot;join event e on u.user_id = e.user_id&quot;    

try:
    final_query = f&quot;&quot;&quot;
with
users as (
    select user_id
    from {userTable}
)
{signed_query}
{login_query}
{pv_query}
{order_query}
{wish_query}
{subscription_query}
{utm_query}
{event_query}
select distinct u.user_id
from users u
{signed_join}
{login_join}
{pv_join}
{order_join}
{wish_join}
{subscription_join}
{utm_join}
{event_join}
{extra_where}
    &quot;&quot;&quot;.replace('\n\n', '\n').replace(&quot;%%&quot;, &quot;%&quot;)


    st.subheader(&quot;쿼리 보기 (복사하세요!)&quot;)
    query = st.code(
        final_query,
        language='sql'
    )
    
    st.markdown(&quot;&quot;&quot;
        &amp;lt;head&amp;gt;
            &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/css/bootstrap.min.css&quot; integrity=&quot;sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm&quot; crossorigin=&quot;anonymous&quot;&amp;gt;
        &amp;lt;/head&amp;gt;
        &amp;lt;body&amp;gt;
            &amp;lt;a href=&quot;https://redash/queries/new&quot; target = &quot;_blank&quot; class=&quot;btn btn-outline-primary&quot; role=&quot;button&quot;&amp;gt;리대시에 쿼리 붙여넣기&amp;lt;/a&amp;gt;
        &amp;lt;/body&amp;gt;
        &quot;&quot;&quot;, unsafe_allow_html=True)

except Exception as e:
    st.error(e)
    pass&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면으로 보면 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;754&quot; data-origin-height=&quot;813&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k1oNn/btrLNwFsuHT/06geXXZCL4JKJCBkVLHIB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k1oNn/btrLNwFsuHT/06geXXZCL4JKJCBkVLHIB1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k1oNn/btrLNwFsuHT/06geXXZCL4JKJCBkVLHIB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk1oNn%2FbtrLNwFsuHT%2F06geXXZCL4JKJCBkVLHIB1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;754&quot; height=&quot;813&quot; data-origin-width=&quot;754&quot; data-origin-height=&quot;813&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저를 추출할 조건을 도메인에 따라 구성하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 기준에 맞는 쿼리를 모듈화해서 조립해 주는 간단한 기능입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타겟은 CRM, CX, UX 등 특정 세그먼트의 유저를 찾고 싶어하는 구성원들입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 지난 글에서 이야기한 queryMaker와 이번 userSelector, 두 가지 서비스만 탑재하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;급 회사 메신저를 통해 공개해 버렸습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목적은 '이런 것이 있습니다' 라는 안내와 아이디어 피드백을 받기 위한 것이었는데요,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;565&quot; data-origin-height=&quot;246&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c73R1A/btrLLAhzcoY/QkiJlLkzWZVYKpqwOmCaZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c73R1A/btrLLAhzcoY/QkiJlLkzWZVYKpqwOmCaZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c73R1A/btrLLAhzcoY/QkiJlLkzWZVYKpqwOmCaZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc73R1A%2FbtrLLAhzcoY%2FQkiJlLkzWZVYKpqwOmCaZk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;565&quot; height=&quot;246&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;565&quot; data-origin-height=&quot;246&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다양한 분들이 좋은 반응을 보여주셨지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예상치 못하게.. 사실은 조금 예상했던 피드백을 하나 받게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&quot;데이터 구조가 다 드러나는데 보안은 괜찮나요?&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;조금은 예상했기 때문에 보안 문제를 피하기 위해 직접 데이터 결과를 보여주지도 않고,&lt;br /&gt;구글 로그인이 필요한 리대시로 쿼리만 들려 보내는 서비스로 구상했지만&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;'데이터 구조' 역시 회사의 중요 정보라는 사실을 간과했던 것이었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;데이터 마트의 주요 테이블 이름과 필드들이 모두 노출되는 것은 사실이기에 바로 보완하기로 하고&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그렇게 데이터 어드민은 phase 2로 접어들게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 편에서는 보안과 새로운 서비스에 대해 이야기해 보겠습니다.&lt;/p&gt;</description>
      <category>etc.</category>
      <category>python</category>
      <category>SQL</category>
      <category>streamlit</category>
      <category>데이터</category>
      <category>데이터분석</category>
      <category>데이터어드민</category>
      <category>데이터엔지니어</category>
      <category>파이썬</category>
      <author>Ramihan</author>
      <guid isPermaLink="true">https://datarami.tistory.com/34</guid>
      <comments>https://datarami.tistory.com/34#entry34comment</comments>
      <pubDate>Sat, 10 Sep 2022 01:32:48 +0900</pubDate>
    </item>
    <item>
      <title>웹알못의 데이터 어드민 사이트 만들기(feat. Streamlit) 2. 페이지 작성</title>
      <link>https://datarami.tistory.com/33</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Streamlit을 띄운 목적이 데이터 애드혹을 줄이는 것이었기 때문에,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 테이블을 조회하는 간단한 쿼리를 누구나 클릭 베이스로 작성할 수 있는 페이지를 가장 먼저 작성해 봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부끄럽지만 전체 코드(일부를 제외한) 를 첨부합니다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1662738387430&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import streamlit as st
import pandas as pd
from datetime import datetime, timedelta

st.set_page_config(page_title=&quot;queryMaker&quot;, page_icon=&quot; &quot;, layout='wide')
st.title(&quot;dat-a-dmin [queryMaker]&quot;)

###

d0_options = {
    &quot;유저, 크리에이터&quot;: [&quot;&quot;],
    &quot;마케팅&quot;: [&quot;&quot;],
    &quot;사이트/앱 로그&quot;: [&quot;&quot;],
    &quot;매출&quot;: [&quot;&quot;],
    &quot;구독&quot;: [&quot;&quot;],
    &quot;글로벌&quot;: [&quot;&quot;],
    &quot;클래스, 키트, 스토어&quot;: [&quot;&quot;],
    &quot;기타&quot;: [None]
}

d0_tu = tuple(d0_options.keys())

depth0 = st.selectbox(
    '어떤 데이터가 필요하신가요?',
    d0_tu   
)

@st.cache(ttl=10800)
def get_tables(tablename):
    engine = db.ps_engine()
    schemas = pd.read_sql(f&quot;desc {tablename}&quot;, con=engine)
    return schemas
    
d0_result = (d0_options.get(depth0))

depth1 = st.selectbox(
    label='어떤 테이블을 확인해 볼까요?',
    options=tuple(d0_result),
)

if depth1==None:
    st.warning(&quot;데이터 팀에 문의해 주세요!&quot;)
else:
    sc = get_tables(depth1)
    
depth2 = st.multiselect(
    '어떤 조건이 필요한가요? (복수 선택)',
    ['날짜 범위', '숫자 범위', '특정 텍스트 포함', 'T/F']
)

try:
    num_col = sc.loc[(sc['Type'].str.contains('int'))|(sc['Type']=='double')|(sc['Type'].str.contains('decimal')), 'Column']
    date_col = sc.loc[(sc['Type']=='date')|(sc['Type'].str.contains('timestamp')), 'Column']
    text_col = sc.loc[(sc['Type'].str.contains('varchar')), 'Column'] 
    bool_col = sc.loc[(sc['Type']=='boolean'), 'Column'] 

    d2_options = {
        &quot;날짜 범위&quot;: date_col,
        &quot;숫자 범위&quot;: num_col,
        &quot;특정 텍스트 포함&quot;: text_col,
        &quot;T/F&quot;: bool_col
    }

    where_sentence = &quot;where 1=1&quot;

    for d2 in depth2:
        globals() ['depth3_'+d2] = st.multiselect(
            f'{d2} 조건 설정할 필드 (복수 선택)',
            d2_options.get(d2)
        )
        
        try:
            for d3 in globals() ['depth3_'+d2]:
                if d2=='날짜 범위':
                    globals() ['depth4_date_'+d3] = st.date_input(
                        f'{d3} 날짜 범위 선택',
                        [datetime.date(datetime.now() - timedelta(days=30)), datetime.date(datetime.now())]
                    )
                    try:
                        start_date = datetime.strftime(globals() ['depth4_date_'+d3][0], '%Y-%m-%d')
                        end_date = datetime.strftime(globals() ['depth4_date_'+d3][1], '%Y-%m-%d')
                        where_sentence += f&quot;\nand date({d3}) between date'{start_date}' and date'{end_date}'&quot;
                        if 'log' in depth1:
                            start_pymd = datetime.strftime(globals() ['depth4_date_'+d3][0], '%Y%m%d')
                            end_pymd = datetime.strftime(globals() ['depth4_date_'+d3][1], '%Y%m%d') 
                            where_sentence += f&quot;\nand p_ymd between '{start_pymd}' and '{end_pymd}'&quot;
                    except:
                        pass 

                if d2=='숫자 범위':
                    globals() ['depth4_num_'+d3] = st.text_input(
                        f'{d3} 숫자 범위 입력 (x~y)'
                    )
                    try:
                        x = globals() ['depth4_num_'+d3].strip().split('~')[0]
                        y = globals() ['depth4_num_'+d3].strip().split('~')[1]
                        where_sentence += f&quot;\nand {d3} between {x} and {y}&quot;
                    except:
                        pass 

                if d2=='특정 텍스트 포함':
                    globals() ['depth4_text_include_'+d3] = st.text_input(
                        f'{d3} 포함할 텍스트'
                    )
                    globals() ['depth4_text_exclude_'+d3] = st.text_input(
                        f'{d3} 제외할 텍스트'
                    )
                    try:   
                        if globals() ['depth4_text_include_'+d3] != '': 
                            where_sentence += f&quot;\nand {d3} like '%{globals() ['depth4_text_include_'+d3]}%'&quot;
                    except Exception as e:
                        st.write(e)
                        pass
                    try:
                        if globals() ['depth4_text_exclude_'+d3] != '':
                            where_sentence += f&quot;\nand {d3} not like '%{globals() ['depth4_text_exclude_'+d3]}%'&quot;
                    except Exception as e:
                        st.write(e)
                        pass

                if d2=='T/F':
                    globals() ['depth4_bool_'+d3] = st.checkbox(
                        f'{d3} 해당 여부'
                    )
                    try:
                        where_sentence += f&quot;\nand {d3} = {globals() ['depth4_bool_'+d3]}&quot;
                    except:
                        pass

            fin_query = f&quot;&quot;&quot;select *
from {depth1}
{where_sentence}
limit 10&quot;&quot;&quot;

        except Exception as e:
            pass

    st.markdown('---')

    try:
        if fin_query:
            st.subheader(&quot;쿼리가 완성되었어요 (복사하세요!)&quot;)
            query = st.code(
                fin_query,
                language='sql'
            )
        try:
            validation = pd.read_sql(fin_query.replace('%', '%%'), engine).head(5)
            st.write(&quot;정상 쿼리입니다.&quot;)
            st.markdown(&quot;&quot;&quot;
                &amp;lt;head&amp;gt;
                    &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/css/bootstrap.min.css&quot; integrity=&quot;sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm&quot; crossorigin=&quot;anonymous&quot;&amp;gt;
                &amp;lt;/head&amp;gt;
                &amp;lt;body&amp;gt;
                    &amp;lt;a href=&quot;https://redash/queries/new&quot; target = &quot;_blank&quot; class=&quot;btn btn-outline-primary&quot; role=&quot;button&quot;&amp;gt;리대시에 쿼리 붙여넣기&amp;lt;/a&amp;gt;
                &amp;lt;/body&amp;gt;
                &quot;&quot;&quot;, unsafe_allow_html=True)
            st.info(&quot;&quot;&quot;
            - 리대시에서 `limit 10`을 지우면 전체 결과를 조회하실 수 있습니다.
            &quot;&quot;&quot;)
            # st.table(validation)
        except Exception as e:
            st.markdown(f&quot;#### 비정상 쿼리입니다.\n```{e}```&quot;)
    except Exception as e:
        pass

except:
    pass&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 요약하면 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock floatLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;880&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6ZGEs/btrLLXDzqTU/aYl9PVWo56GpLLZWk5y8nK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6ZGEs/btrLLXDzqTU/aYl9PVWo56GpLLZWk5y8nK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6ZGEs/btrLLXDzqTU/aYl9PVWo56GpLLZWk5y8nK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6ZGEs%2FbtrLLXDzqTU%2FaYl9PVWo56GpLLZWk5y8nK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;650&quot; height=&quot;880&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;880&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 도메인 선택&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 테이블 선택&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 쿼리 조건 선택&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; - 날짜 범위&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; - 숫자 범위&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; - 특정 텍스트 포함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; - T/F (bool)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 선택한 조건 입력&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 완성된 쿼리 활용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;647&quot; data-origin-height=&quot;333&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/O1zd2/btrLLWkma4k/o7PKJzZ6wuP5xsFMFTgqMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/O1zd2/btrLLWkma4k/o7PKJzZ6wuP5xsFMFTgqMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/O1zd2/btrLLWkma4k/o7PKJzZ6wuP5xsFMFTgqMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FO1zd2%2FbtrLLWkma4k%2Fo7PKJzZ6wuP5xsFMFTgqMK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;647&quot; height=&quot;333&quot; data-origin-width=&quot;647&quot; data-origin-height=&quot;333&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현된 화면은 예상보다 단촐합니다^^;;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요점은, 데이터 마트를 도메인 별로 분류해서 각 도메인의 주요 테이블만 미리 정해 주면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나머지는 로직에 따라 모든 필드의 where절을 구성할 수 있다는 부분입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리를 어느 정도 쓰실 줄 아는 분들에게는 별 것 아니지만 where절을 구성하는 것도 어려운 분들께 최소한의 가이드를 잡아드리는 것을 목적으로 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 이 기능을 사용하기 위해서는 테이블마다 각 필드가 무엇을 의미하는지 정보가 필요한데요,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희는 이 부분을 datahub로 보완하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://datahubproject.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://datahubproject.io/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1662739109107&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;A Metadata Platform for the Modern Data Stack | DataHub&quot; data-og-description=&quot;DataHub is a data discovery application built on an extensible metadata platform that helps you tame the complexity of diverse data ecosystems.&quot; data-og-host=&quot;datahubproject.io&quot; data-og-source-url=&quot;https://datahubproject.io/&quot; data-og-url=&quot;https://datahubproject.io/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://datahubproject.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://datahubproject.io/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;A Metadata Platform for the Modern Data Stack | DataHub&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;DataHub is a data discovery application built on an extensible metadata platform that helps you tame the complexity of diverse data ecosystems.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;datahubproject.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 편에서는 유저 리스트를 추출할 수 있는 서비스와 실제 오픈 후 문제에 대해 회고하겠습니다.&lt;/p&gt;</description>
      <category>etc.</category>
      <category>python</category>
      <category>SQL</category>
      <category>streamlit</category>
      <category>데이터</category>
      <category>데이터분석</category>
      <category>데이터어드민</category>
      <category>데이터엔지니어</category>
      <category>데이터팀</category>
      <category>파이썬</category>
      <author>Ramihan</author>
      <guid isPermaLink="true">https://datarami.tistory.com/33</guid>
      <comments>https://datarami.tistory.com/33#entry33comment</comments>
      <pubDate>Sat, 10 Sep 2022 01:00:28 +0900</pubDate>
    </item>
    <item>
      <title>웹알못의 데이터 어드민 사이트 만들기(feat. Streamlit) 1. 환경 구축</title>
      <link>https://datarami.tistory.com/32</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Streamlit을 사용하기 위한 환경은 크게 어렵지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;pip install streamlit&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PIP 설치와&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;streamlit hello&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;실행 명령 한 줄이면 로컬에서도 웹앱을 띄울 수 있으니까요.&lt;/div&gt;
&lt;div&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1204&quot; data-origin-height=&quot;672&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Z33u9/btrLNvzKmV8/96LpigIuT0VTr6d1mwJMK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Z33u9/btrLNvzKmV8/96LpigIuT0VTr6d1mwJMK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Z33u9/btrLNvzKmV8/96LpigIuT0VTr6d1mwJMK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZ33u9%2FbtrLNvzKmV8%2F96LpigIuT0VTr6d1mwJMK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1204&quot; height=&quot;672&quot; data-origin-width=&quot;1204&quot; data-origin-height=&quot;672&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 말은 즉, 파이썬과 외부 네트워크만 가능하면 누구나 접속할 수 있는 사이트를 만들 수 있다는 뜻입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS나 GCP 인스턴스 한 대로도 운영할 수 있지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 저희 데이터 팀에서는 K8S 환경으로 대부분의 서비스를 통합했기 때문에 클러스터에 띄우기로 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 가장 먼저 필요한 건 도커 이미지입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Streamlit 컨테이너를 구성할 재료입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 python 3.9 이미지를 기반으로 streamlit 외 데이터 어드민에 사용할 라이브러리들을 설치했습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1662737198843&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;FROM python:3.9-slim

COPY requirements.txt .

RUN pip install -U --no-cache-dir -r requirements.txt

RUN mkdir /streamlit

WORKDIR /streamlit&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 템플릿입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스 컨테이너와 configmap, pvc 등 여러 요소들을 설정하기 위한 기초 설계도입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 streamlit을 k8s에 배포하기 위해 가장 기본적인 설정들만 남긴 deployment 템플릿 예시입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1662737502269&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: streamlit
  labels:
    app: streamlit
spec:
  selector:
    matchLabels:
      app: streamlit
  replicas: 1
  template:
    metadata: 
      labels:
        app: streamlit
    spec:
      - name: streamlit
        image: {streamlit 이미지}
        imagePullPolicy: Always
        ports:
          - containerPort: 8501
        volumeMounts:
          - name:  streamlit
            mountPath: /streamlit
        args:
          - bash
          - -c
          - |-
            exec \
            streamlit run /streamlit/home.py
      volumes:
        - name: streamlit
          emptyDir: {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 service와 ingress까지 설정하고 드디어 home.py를 작성했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1662737770453&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import streamlit as st

st.set_page_config(page_title=&quot;Home&quot;, page_icon=&quot; &quot;)

st.title(&quot;dat-a-dmin&quot;)

st.markdown(&quot;&quot;&quot;
`data + admin`

클릭 베이스로 UI 순서에 따라 정보를 기입하면 최종 결과물을 확인할 수 있습니다.
&quot;&quot;&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 편에서는 본격적으로 streamlit을 이용해 구성한 데이터 어드민 페이지에 대해 소개하겠습니다.&lt;/p&gt;</description>
      <category>etc.</category>
      <category>k8s</category>
      <category>streamlit</category>
      <category>데이터</category>
      <category>데이터분석</category>
      <category>데이터엔지니어</category>
      <category>도커</category>
      <category>쿠버네티스</category>
      <category>파이썬</category>
      <author>Ramihan</author>
      <guid isPermaLink="true">https://datarami.tistory.com/32</guid>
      <comments>https://datarami.tistory.com/32#entry32comment</comments>
      <pubDate>Sat, 10 Sep 2022 00:39:56 +0900</pubDate>
    </item>
    <item>
      <title>웹알못의 데이터 어드민 사이트 만들기(feat. Streamlit) 0.Prologue</title>
      <link>https://datarami.tistory.com/31</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;스타트업의 데이터 팀에는 수많은 리퀘스트가 들어옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 중에서 단순 추출과 개인정보 처리는 그 중요도와는 별개로, 요청은 많고 얻어가는 보람은 적은 일에 속하죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조직에 직접 인사이트를 제공하는 생산적인 분석을 하고 싶어 데이터 분석가가 되었는데, 이런 애드혹이 반복되면 결국 내가 데이터 분석가인가? 추출가인가? 자괴감이 들게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 문제를 해결하기 위해 On-call(당번), 리퀘스트 워크플로우, 백로그 프로세스 개선 등 다양한 시도를 했지만&lt;br /&gt;문제는 제품과 비즈니스 실무진의 데이터 접근성이 떨어지는 것이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분석용 데이터 마트가 갖추어져 있고 리대시나 태블로를 통해 원하는 구성원이라면 누구나 데이터에 접근할 수 있는데 왜 접근성이 떨어지는가? 라고 반문했을 때는 아래와 같은 이유들을 찾을 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 리대시는 SQL에 능숙하지 않으면 사용할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. SQL 교육을 하더라도 데이터 구조나 테이블 명세에 익숙해지는 데 시간이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 태블로는 시각화를 위해 가공된 데이터라서 유연성이 떨어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 태블로에서 모든 raw data를 제공할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 데이터 교육의 경우 적극적으로 원하시는 분들도 많이 있지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각자의 영역에서 모든 시간과 노력을 쏟고 있는 분들께 데이터까지 꼭 배워야 한다고 강요할 수는 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 거쳐 온 여러 데이터 조직들이 모두 비슷한 문제를 겪고 있었기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 정말로 해결하고 싶었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&quot;데이터 어드민을 구축하자..!&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 나는 뭐다? 웹알못이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 프레임워크를 다루지 못하는 상태에서 새로 공부를 해야겠다 다짐하던 중,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Streamlit이라는 서비스를 알게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://streamlit.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://streamlit.io/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1662735712786&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Streamlit &amp;bull; The fastest way to build and share data apps&quot; data-og-description=&quot;Streamlit is an open-source app framework for Machine Learning and Data Science teams. Create beautiful web apps in minutes.&quot; data-og-host=&quot;streamlit.io&quot; data-og-source-url=&quot;https://streamlit.io/&quot; data-og-url=&quot;https://streamlit.io/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/pyXu4/hyPJ2MMLKU/kEPKSKuUtTOVHJikT4qeaK/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/XWE0N/hyPJSi41hx/7rzJEsjNtfICEeaAcJnfkk/img.jpg?width=280&amp;amp;height=150&amp;amp;face=0_0_280_150&quot;&gt;&lt;a href=&quot;https://streamlit.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://streamlit.io/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/pyXu4/hyPJ2MMLKU/kEPKSKuUtTOVHJikT4qeaK/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/XWE0N/hyPJSi41hx/7rzJEsjNtfICEeaAcJnfkk/img.jpg?width=280&amp;amp;height=150&amp;amp;face=0_0_280_150');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Streamlit &amp;bull; The fastest way to build and share data apps&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Streamlit is an open-source app framework for Machine Learning and Data Science teams. Create beautiful web apps in minutes.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;streamlit.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Streamlit은 파이썬 코드 기반으로 웹앱을 쉽게 만들 수 있는 라이브러리입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주로 데이터 분석/ML 프로토타입 용도로 쓰이고 있는 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제공하는 막강한 차트 기능만 보더라도 익히 알 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;814&quot; data-origin-height=&quot;802&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckSXj9/btrLLWLmMyF/aImp9G56Kz0KC0bQwz95KK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckSXj9/btrLLWLmMyF/aImp9G56Kz0KC0bQwz95KK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckSXj9/btrLLWLmMyF/aImp9G56Kz0KC0bQwz95KK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckSXj9%2FbtrLLWLmMyF%2FaImp9G56Kz0KC0bQwz95KK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;814&quot; height=&quot;802&quot; data-origin-width=&quot;814&quot; data-origin-height=&quot;802&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Streamlit 라이브러리를 활용해서 만들고자 하는 최종 목표는 아래와 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 개인정보 처리 시스템&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. UI 기반 데이터 추출 시스템&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. UI 기반 간단한 비주얼라이제이션&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 데이터 리소스 검색기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 편부터 실제로 Streamlit 환경을 구축한 과정을 설명하겠습니다.&lt;/p&gt;</description>
      <category>etc.</category>
      <category>streamlit</category>
      <category>데이터</category>
      <category>데이터분석</category>
      <category>데이터어드민</category>
      <category>데이터엔지니어</category>
      <category>데이터자판기</category>
      <category>데이터팀</category>
      <category>리퀘스트</category>
      <category>애드혹</category>
      <category>파이썬</category>
      <author>Ramihan</author>
      <guid isPermaLink="true">https://datarami.tistory.com/31</guid>
      <comments>https://datarami.tistory.com/31#entry31comment</comments>
      <pubDate>Sat, 10 Sep 2022 00:08:55 +0900</pubDate>
    </item>
    <item>
      <title>AWS EMR Prestosql(Trino) + Redash 업그레이드</title>
      <link>https://datarami.tistory.com/30</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;기존 환경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- EMR 6.2.0 + PrestoSql 343&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Re:dash 8.0.0&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Terraform을 통한 EMR Trino 업그레이드 + File Access Control 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- EMR 6.4.0 + Trino 359 업그레이드를 위한 Terraform Configuration 파일 수정사항&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; - Classification의 'prestosql'을 'trino'로 변경&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; - 일부 설정값 호환 여부 확인&lt;/p&gt;
&lt;pre id=&quot;code_1633773553408&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;---- 변경 전
{
    &quot;Classification&quot;: &quot;prestosql-connector-hive&quot;,
    &quot;Properties&quot;: {
      &quot;hive.metastore&quot;: &quot;glue&quot;,
      &quot;hive.metastore.glue.datacatalog.enabled&quot;: &quot;true&quot;,
      &quot;hive.s3-file-system-type&quot;: &quot;PRESTO&quot;
    }
 }
 
 ----변경 후 
  {
    &quot;Classification&quot;: &quot;trino-connector-hive&quot;,
    &quot;Properties&quot;: {
      &quot;hive.metastore&quot;: &quot;glue&quot;,
      &quot;hive.s3-file-system-type&quot;: &quot;TRINO&quot;
    }
 }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- File Access Control 설정&lt;/p&gt;
&lt;pre id=&quot;code_1633773835291&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Ubuntu -&amp;gt; /usr/lib/trino/etc 경로에 access-control.properties 생성
access-control.name=file
security.config-file=etc/rules.json
security.refresh-period=1s&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;- rules.json 파일 예시&lt;/p&gt;
&lt;pre id=&quot;code_1633773977671&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 실제 json 파일 작성 시 주석 모두 제거

{
  &quot;catalogs&quot;: [
    { # guest 유저의 system 카탈로그 접근을 거부한다
      &quot;user&quot;: &quot;guest&quot;, 
      &quot;catalog&quot;: &quot;(system)&quot;,
      &quot;allow&quot;: &quot;none&quot;
    },
    { # guest, general_user 유저에게 읽기 전용으로 모든 카탈로그 접근을 허용한다.
      &quot;user&quot;: &quot;guest|general_users&quot;,
      &quot;catalog&quot;: &quot;.*&quot;,
      &quot;allow&quot;: &quot;read-only&quot;
    },
    { # admin 유저에게 모든 카탈로그의 전체 권한을 허용한다.
      &quot;user&quot;: &quot;admin&quot;,
      &quot;catalog&quot;: &quot;.*&quot;,
      &quot;allow&quot;: &quot;all&quot;
    }
  ],
  &quot;tables&quot;: [ # 테이블 단위 권한 관리
    { # admin 유저에게 모든 테이블의 전체 권한을 부여한다.
      &quot;user&quot;: &quot;admin&quot;,
      &quot;privileges&quot;: [&quot;SELECT&quot;, &quot;DELETE&quot;, &quot;INSERT&quot;, &quot;OWNERSHIP&quot;]
    },
    { # 전체 유저에게 customers가 포함된 테이블, users 테이블, shipping으로 시작하는 테이블의 select 권한만 부여하고, 
      # 지정된 컬럼을 block 처리한다.
      &quot;table&quot;: &quot;.*customers.*|users|shipping.*&quot;,
      &quot;privileges&quot;: [&quot;SELECT&quot;],
      &quot;columns&quot;: [
        {
          &quot;name&quot;: &quot;name&quot;,
          &quot;mask&quot;: &quot;'blocked'&quot;
        },
        {
          &quot;name&quot;: &quot;email&quot;,
          &quot;mask&quot;: &quot;'blocked'&quot;
        },
        {
          &quot;name&quot;: &quot;phone&quot;,
          &quot;mask&quot;: &quot;'blocked'&quot;
        },
        {
          &quot;name&quot;: &quot;address&quot;,
          &quot;mask&quot;: &quot;'blocked'&quot;
        }
      ]
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Redash V8 -&amp;gt; V10 Upgrade&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;- Re:dash V10 업그레이드 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- UX 개선&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Trino Data Connector 추가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;&lt;a href=&quot;https://redash.io/help/open-source/setup&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://redash.io/help/open-source/setup&lt;/a&gt; Redash V8 AWS instance AMI 기준&amp;gt;&lt;/p&gt;
&lt;pre id=&quot;code_1633774561272&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Redash V8 인스턴스 접속 후 아래 경로에서 도커 컴포즈 파일 편집
/opt/redash/docker-compose.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. service / scheduler / environment의 QUEUES와 WORKERS_COUNT 항목 삭제 (environment 항목이 비어있으면 함께 삭제)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. service 항목에 아래 RQ job 추가&lt;/p&gt;
&lt;pre id=&quot;code_1633774666747&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;worker:
  &amp;lt;&amp;lt;: *redash-service
  command: worker
  environment:
    QUEUES: &quot;periodic emails default&quot;
    WORKERS_COUNT: 1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. docker-compose run --rm server manage db upgrade 명령어로 DB 업그레이드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. docker-compose up --force-recreate --build 명령어로 전체 업그레이드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1915&quot; data-origin-height=&quot;927&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kyHud/btrhdwXgEzo/0FygiqUpWycISZzDzebyWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kyHud/btrhdwXgEzo/0FygiqUpWycISZzDzebyWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kyHud/btrhdwXgEzo/0FygiqUpWycISZzDzebyWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkyHud%2FbtrhdwXgEzo%2F0FygiqUpWycISZzDzebyWK%2Fimg.png&quot; data-origin-width=&quot;1915&quot; data-origin-height=&quot;927&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* References&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://github.com/getredash/redash/blob/release/10.0.x/CHANGELOG.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/getredash/redash/blob/release/10.0.x/CHANGELOG.md&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1633774935148&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - getredash/redash: Make Your Company Data Driven. Connect to any data source, easily visualize, dashboard and share your&quot; data-og-description=&quot;Make Your Company Data Driven. Connect to any data source, easily visualize, dashboard and share your data. - GitHub - getredash/redash: Make Your Company Data Driven. Connect to any data source, e...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/getredash/redash/blob/release/10.0.x/CHANGELOG.md&quot; data-og-url=&quot;https://github.com/getredash/redash&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bfNpEG/hyLUrifBfG/P8QLw14Z4xu6y4ggFF86s1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/getredash/redash/blob/release/10.0.x/CHANGELOG.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/getredash/redash/blob/release/10.0.x/CHANGELOG.md&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bfNpEG/hyLUrifBfG/P8QLw14Z4xu6y4ggFF86s1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - getredash/redash: Make Your Company Data Driven. Connect to any data source, easily visualize, dashboard and share your&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Make Your Company Data Driven. Connect to any data source, easily visualize, dashboard and share your data. - GitHub - getredash/redash: Make Your Company Data Driven. Connect to any data source, e...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://github.com/getredash/redash/issues/5531&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/getredash/redash/issues/5531&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1633774947367&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;V10 beta crashes on dashboards.options column missing when upgrading from docker v8/v9-beta  &amp;middot; Issue #5531 &amp;middot; getredash/redash&quot; data-og-description=&quot;Issue Summary Steps to Reproduce Upgrade from a self hosted v8 docker image to v10 beta change docker file and launch docker container using v10 upgrade instructions get a server error when trying ...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/getredash/redash/issues/5531&quot; data-og-url=&quot;https://github.com/getredash/redash/issues/5531&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/snPN0/hyLUrP5K66/wGRcPANJczTO5w3lGqlvn0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/getredash/redash/issues/5531&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/getredash/redash/issues/5531&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/snPN0/hyLUrP5K66/wGRcPANJczTO5w3lGqlvn0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;V10 beta crashes on dashboards.options column missing when upgrading from docker v8/v9-beta &amp;middot; Issue #5531 &amp;middot; getredash/redash&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Issue Summary Steps to Reproduce Upgrade from a self hosted v8 docker image to v10 beta change docker file and launch docker container using v10 upgrade instructions get a server error when trying ...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>etc.</category>
      <author>Ramihan</author>
      <guid isPermaLink="true">https://datarami.tistory.com/30</guid>
      <comments>https://datarami.tistory.com/30#entry30comment</comments>
      <pubDate>Sat, 9 Oct 2021 19:22:35 +0900</pubDate>
    </item>
  </channel>
</rss>