Minwoo writings

DBT (Data Build Tool) - 101

SQL for Product Level

들어가며,

  • 데이터 분석가로 일하며 겪은 문제 중 하나는
    • 온전한 제품을 제공하고 운영하는 과정은 일반적인 개발자와 다름없음에도 불구하고 기반한 문서나 코드의 퀄리티가 개발을 업으로 하는 사람들과 큰 차이를 보인다는 점
    • 분석의 과정이 정형화되어 있지 않고 / 다양한 모습의 산출물로 요구되기 때문이기도 하고 / 기반한 기술들이 아직 성숙하지 않은 문제도 있기 때문
    • 개인적으로 SQL 작성 시에 이러한 문제와 한계를 많이 체감했다.
  • SQL이 업무의 산출물인 경우
    • 주로 성능과 안정성을 고려해 작성되는 SQL, 가독성이 떨어지며 유지보수 또한 어려움
    • 하나의 코드 뭉치에 복잡한 로직이 엉켜있어 일부의 로직 수정이 코드 전반에 걸친 대대적인 수정을 필요로 하고, 전체를 재작성하는 것이 더 빠른 상황이 자주 나옴.
  • 문제 해결을 위해
    • 작성된 로직에 오류는 없는지 동료의 리뷰가 필요했고,
    • 변경된 로직에 따라 수정된 내역을 보관하고 기록할 수 있도록 버전 관리가 필요했고,
    • 이상 데이터가 포함되어 오류가 발생하는지 모니터링도 필요하며,
    • 여타 다른 개발 언어와 같은 수준의 편의 기능들이 필요했다.
    • 동시에 동료, 외부 조직의 접근성을 높일 수 있도록 출력 결과에 대한 안내, 문서화가 필요했다.
  • dbt는 이런 문제들을 모두 해결하기 위한 도구였고 큰 기대를 하고 보기 시작했다.

About dbt

  • ELT(Extract, Load, Transform) 중에서 T(Transform)를 위해 제작된 도구
    • ETL → ELT로의 변화, 혹은 그 이후의 개선을 위해 DBT와 같은 Transform만을 위한 도구가 대두된 것
    • 외부 데이터 소스로부터 데이터를 추출하거나 적재하는 기능 부재
    • 이미 적재되어 있는 데이터를 조회하고 수정하는 데에 최적화된 도구 large
      • DBT가 제안하는 Data Platform Stack은 [External Source → Extract & Loader → Warehouse → Consumer] 크게 4개 영역으로 나누고 Warehouse에서 일어나는 모든 Transform을 DBT가 담당하는 것
      • DBT는 이러한 데이터 가공 과정을 Modeling이라 표현
  • How it works large
    • SQL, YML로 구성
      • SQL 파일에 모델링 작성
      • YML 파일에 SQL 파일의 compile에 필요한 설정 값을 작성
    • Compiler와 Runner로 구성된 CLI Tool
      • Compiler
        • SQL, YML 파일을 Compile하여 또 다른 SQL 생성
        • Jinja Template Engine 사용
      • Runner
        • 컴파일된 SQL을 연결된 warehouse에 전달하여 테이블 생성
        • 실행 시 의존성을 가지는 모델을 파악해 자동적으로 DAG을 작성, 이에 따라 실행 계획을 만들고 순서대로 실행
    • 오픈소스, Pyhton으로 제작, PIP를 통해 설치 가능
  • Why & When
    • SQL을 코드처럼 작성해야할 때 필요함
    • 작성된 SQL의 변경사항을 공유하거나 충돌을 막기 힘듦 → 버전 관리가 필요
    • 추출할 지표에 대한 문서화와 공유는 SQL로 이뤄지기 힘듦 → 코드와 연동한 문서화 도구가 필요
    • 작성된 SQL을 재활용하기 힘듦 → 부분부분 독립적으로 모듈화하여 재활용
    • 작성된 SQL을 테스트하기 힘듦 → SQL에 대한 테스트 도구 필요
  • Avaliable Databases
    • [BigQuery, Snowflake, Postgres, Redshift, Apache Spark, Databricks, Presto] 연결 지원

기능 및 구성 요소

  • Project
    • dbt 프로젝트를 의미
    • 프로젝트의 설정은 dbt_project.yml에 기록
      • 프로젝트의 이름, 데이터세트의 이름 등의 설정
      • materialization 설정을 프로젝트/폴더 단위로 설정
      • 타겟 설정 (환경 설정의 일종)
    • 프로젝트의 생성은 아래와 같이 실행
      1
      
        $ dbt init foo 
      
    • 실행 결과로 아래와 같은 구조의 폴더/파일이 생성됨
  • Model
    • select 구문이 포함된 sql 파일
    • Jinja Template 사용하여 SQL로 표현이 어려운 코드 작성 가능하다.
    • 예시
      1
      2
      3
      4
      5
      6
      
        {{ set columns = ['a', 'b', 'c'] }}
        select
            {% for column_name in columns %}
            {{ column_name }},
            {% endfor %}
        from source_data
      
    • 작성한 모델의 실행은 아래와 같이 입력
      1
      
        $ dbt run --models foo
      
    • 최종적으로 생성될 테이블의 이름은 모델의 이름과 동일하게 설정됨
      • 작업 환경에서의 폴더 구조와 다르게 데이터세트 하나에 테이블이 저장되므로 모델의 네이밍이 중요.
      • 따라서, 모델 상위 디렉터리의 구조를 모델 이름에 중복해 기술해주는 것을 권장
        • 예를 들어,
          • bad : “foo”
          • good : “stg_payments__foo”
  • Materialization
    • Model을 저장 혹은 유지하는 방법
    • 기본적으로 [table, view, incremental, ephemeral] type이 제공됨
      • table : 테이블을 신규 생성하거나 기존 테이블이 존재하는 경우 재구축
      • view : view table 생성
      • incremental : 테이블을 신규 생성하거나 기존 테이블이 존재하는 경우 신규 데이터를 insert하거나 update
      • ephemeral : 실제 테이블을 생성하지 않고 다른 모델에서 dependency로 사용할 수 있도록 함
    • 예시1. table 생성, date 컬럼에 파티션 추가 설정
      1
      2
      3
      4
      5
      6
      7
      8
      9
      
        {{ config(
            materialized='table',
            partition_by={
              "field": "date",
              "data_type": "date"
            }
        )}}
        select * 
        from `project.dataset.table`
      
    • 예시2. incremental 테이블 생성 혹은 업데이트, date 컬럼에 파티션 추가 설정
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      
        {{ config(
            materialized='incremental',
            incremental_strategy='insert_overwrite',
            partition_by={
              "field": "date",
              "data_type": "date"
            }
        )}}
        select * 
        from `project.dataset.table`
        {% if is_incremental() %}
        where
            date >= (select max(date) from {{ this }})
        {% endif %}
      
      • incremental model은 이미 생성된 테이블이 존재하는 경우, is_incremental()의 반환값이 true가 되어 하위의 구문이 실행됨. 예시에서는 where 구문에 일자를 조건으로 건 것.
      • 최초 실행 시에는 fully refresh, 이후 실행 시에는 조회 범위의 제한을 두어 일부만 가져와 업데이트하는 방식.
      • incremental_strategy의 설정에 따라 병합하거나 덮어씌울 수 있음
    • 기본적으로 제공되는 네 가지 materialization 외에도 custom materialization을 작성할 수 있음
  • Test
    • Model이 의도에 맞게 작성되었는지 테스트하기 위한 도구
    • 아래와 같이 실행
      1
      
        $ dbt test
      
    • 크게 두가지 테스트 방법이 존재 - [Schema, Data]
      • Schema Test
        • unique, not_null, accepted_values, relationships 둥 지정한 컬럼의 값이 조건을 만족하는지 확인
        • Model이 소속되어 있는 폴더 내의 schema.yml에 설정
        • 기본적으로 제공되는 macro외에도 custom macro를 직접 만들어 테스트 가능
        • 예시
          1
          2
          3
          4
          5
          6
          7
          8
          9
          
            version: 2
              
            models:
              - name: orders
                columns:
                  - name: order_id
                    tests:
                      - unique
                      - not_null
          
      • Data Test
        • tests directory의 sql을 실행 후 실행 결과의 row 유무에 따라 테스트 실패를 판단
        • 예시
          1
          2
          3
          4
          5
          6
          
            select
                order_id,
                sum(amount) as total_amount
            from {{ ref('stg_payments') }}
            group by 1
            having not(total_amount >= 0)
          
  • Documentation
    • 제작한 모델에 대한 문서를 생성한다.
    • 생성된 문서는 웹 페이지 형태로 제공됨
    • 아래 명령을 실행해 문서를 생성함
      1
      
        $ dbt docs 
      
    • 생성된 문서의 예시 생성한 모델에 대한 설명, 컬럼 안내, SQL을 조회할 수 있다. 생성한 모델의 의존성을 파악할 수 있도록 시각화하여 제공한다.
  • Macro
    • 다수 Model에서 재사용할 수 있도록 작성된 Jinja 코드, 함수의 일종
    • 예시
      • cents_to_dollars.sql
        1
        2
        3
        
          {% macro cents_to_dollars(column_name, precision=2) %}
              ({{ column_name }} / 100)::numeric(16, {{ precision }})
          {% endmacro %}
        
        • macro의 구현 또한 sql 파일로 작성한다.
      • my_model.sql
        1
        2
        3
        4
        
          select
              id as payment_id,
              {{ cents_to_dollars('amount') }} as amount_usd
          from app_data.payments
        
        • 작성해둔 cents_to_dollars macro를 모델에서 사용하기 위해서는 jinja template으로 감싸 사용한다.
      • 위 모델을 컴파일하면 아래와 같은 결과
        1
        2
        3
        4
        
          select
              id as payment_id,
              (amount / 100)::numeric(16, 2) as amount_usd
          from app_data.payments
        
    • Macro를 위한 Package Manager가 제공됨
      • dbt_utils와 같이 타 사용자들이 제작한 macro가 dbt Hub을 통해 공유됨
      • 프로젝트 내의 package.yml 파일에 아래와 같이 설정한 뒤,
        1
        2
        3
        
          packages:
            - package: dbt-labs/dbt_utils
              version: 0.7.0
        
      • 아래 명령어를 통해 설치 진행
        1
        
          $ dbt deps 
        

실행

  • 모델을 테이블로 만드는 모든 과정은 Run을 통해서 진행
  • Arguments
    • profile : 연결할 profile의 설정, dbt_project.yml의 값을 overriding
    • vars : macro 등에서 사용될 변수를 전달
    • models
      • 특정 모델을 선택하여 실행한다.
      • + 기호를 사용하여 지정한 모델의 전/후 DAG을 일괄 실행하도록 설정할 수 있다.
      • 예시1. my_first_dbt_model model 실행
        1
        
          $ dbt run --models my_first_dbt_model
        
        • 결과로 my_first_dbt_model 테이블이 생성된다.
      • 예시2. my_first_dbt_model model과 my_first_dbt_model에 의존성을 가지는 모든 모델 실행
        1
        
          $ dbt run --models my_first_dbt_model+
        
        • 결과로 my_first_dbt_model, my_second_dbt_model 모델이 생성된다.
      • 예시3. my_second_dbt_model model과 my_second_dbt_model이 의존성을 가지는 모든 모델 실행
        1
        
          $ dbt run --models +my_second_dbt_model
        
        • 결과로 my_first_dbt_model, my_second_dbt_model 모델이 생성된다.
    • full-refresh
      • incremental model인 경우 drop 후 rebuild 하도록 설정한다.
  • Scheduling
    • dbt 자체적으로 Scheduling Run하는 방법이 없으므로 Airflow 등의 외부의 도구가 필요
    • dbt의 제작자인 dbt labs가 운영하는 dbt cloud는 이러한 scheduling tool을 제공

모델 구성

  • dbt의 모델링 과정에서 느슨하게 약속된 몇 가지 개념/용어들이 존재 (강제 사항이 아님)

  • Model / Modeling
    • Transform이 완료된 데이터 / Transform 과정을 의미함
  • Source Model
    • Raw Data를 의미함
  • Staging Model
    • Source와 1:1 대응하는 모델을 의미
    • 데이터를 깔끔하게 정리하거나 표준화하는 등의 단순한 작업을 거친 모델
  • Intermediate Model
    • Staging Model에서 Reporting 등의 Consumer가 직접 사용하는 최종 Model 사이의 모델을 의미
    • 이 부분부터 정의가 애매한 편 혹은 자유도가 높음. 프로젝트 혹은 용도에 따라 맞춰 사용
  • Fact Model
    • 지표 저장을 위한 모델
  • Dimension Model
    • Dimension 저장을 위한 모델
  • Source / Staging / Fact / Dimension 으로 구성된 모델 흐름의 예시 large
    • 예시와 같이 모델들을 구성하는 과정은 자유로운 편, 위와 같은 구성이 강제적이지 않다.
    • 공식 문서의 FAQs → Example Project를 확인해보면 Source, Staging Model 까지는 공통, 이후부터의 구현은 프로젝트에 따라 다름

dbt Cloud

  • Managed dbt service
  • 1인 사용 시 무료, 팀 협업 시 유료
  • 장점 : 간편하게 설정하고 실행할 수 있는 custom running environment, job scheduling, logging, documentation deploy
  • 단점 : 일반적인 IDE에 비해 떨어지는 편집기, 가끔 보이는 버그 / 불안정한 동작, 아쉬운 UI/UX, 터미널을 통한 환경 접근 불가능 → custom macro 작성시 상당히 불편함

    편집기 dbt가 실행될 환경에 대한 설정 실행된 job들의 목록 실행된 job의 상세 정보 설정, 프로젝트나 팀메이트 초대 등이 가능


사용 후 의견

  • 비용과 성능 걱정이 적다.
    • BigQuery, Snowflake, Redshift에서라면 비용/성능 고민없이 다수의 transform을 만들 수 있게 될 것
    • 단순히 컴파일/명령을 전달하는 도구이므로 dbt 동작이 위해 큰 퍼포먼스를 요구하지 않는다. → 큐잉, 밸런싱 등의 엔지니어링 부담 없이 운영 가능하다.
  • 데이터 조직의 기능을 개선할 도구
    • 모델링 작업을 통해 데이터를 개선한다.
      • 데이터 조직 내에서도 쿼리를 작성하고 공유하거나 유지보수하는 것이 상당히 원시적인 경우가 많다.
      • dbt는 버전 관리, 모듈화, 모델링의 레이어 구분 등. 운영에 필수적인 요소를 갖추고 있다. 일반적인 데이터 조직에서 이러한 필수적인 요소를 잘 갖추지 못한 경우가.. 적지 않을 것이다.
    • 테스트를 통해 신뢰를 구축한다.
      • 데이터 조직 내에서도 작성한 쿼리의 결과를 믿지 못하는 경우가 많다.
      • 잘못된 데이터의 유입이나 잘못된 쿼리 등등, 스스로를 믿는 좋은 방법이 될 것이다..
    • 문서화를 통해 공유를 용이하게 한다.
      • 작성한 마트로의 물리적인 접근은 쉽다해도 마트에 대한 안내 없이는 외부 조직에서 쉽게 사용할 수 없다.
      • 데이터 리니지, 테이블이나 컬럼 등에 대한 안내가 외부 조직의 접근성을 높이는데 도움이 될 것이다.
  • Transform에 진심인 편..
    • 기존 ETL, ELT 작업을 관리하던 Job이 있다 해도, 완전히 독립적인 T만을 지향한다.
    • 기존 데이터세트에 덮어씌우는 대신 dbt 전용의 데이터세트를 구성한 뒤, merge하는 방식을 권장 / merge는 dbt가 아닌 다른 도구로..
    • Transform 과정에서 stateless를 극단적으로 유지하는 것을 권장
    • 원론적으로는 동의하고 맞는 말이지만, 레거시와 병합하는 과정에서는 괴로울 것..
  • 다소 불편한 UX
    • SQL 작성 시 문법 오류 확인이 어렵다.
      • 컴파일 결과를 dry run하기 전까지는 작성한 쿼리의 오류 여부를 확인할 수 없다. 따라서 작성→컴파일→수정→컴파일 과정을 불필요하게 반복하게 된다.
      • materialization의 구현을 직접 확인하기 전까지는 실제 컴파일 결과를 예상하기 어렵다. 사용자는 select statement만 작성하지만 실제 컴파일 결과에는 drop / create 등의 구문이 추가된다.
    • 컴파일 결과를 볼 때 불편하다.
      • models 폴더에 작업하고 동일한 이름의 파일로 target 폴더에 결과가 저장된다. 오류를 수정하고 컴파일 결과 파일을 수정하는 경우가 허다하다.
    • 매크로 작업이 다소 불편하다.
      • 기존 사용 가능한 매크로에 대한 안내가 부족해 일일히 코드 저장소를 찾아봐야 한다.
      • Python으로 작성된 adapter 기능의 안내 또한 매우 부족하며, custom으로 만들기 어렵다. 따라서 다소 복잡한 요구사항의 macro를 구현하는데에 제한이 크다.
      • 매크로 작성 시의 오류를 디버깅하는 것이 어렵다.
      • Package Manager가 제공되나.. npm pip 등과 비교해 사용 방법이 난해한 편

References


Airflow ExternalTaskSensor 사용 방법

DAG과 DAG 사이 연결하기

Airlflow Task의 upstream, downstream 설정을 통해 Task 실행 순서를 설정할 수 있는 것과 유사하게 DAG과 DAG 사이에서도 실행 순서를 설정할 필요가 있는 경우가 있다. 이 경우를 위해 Cross-DAG Dependencies가 제공되나, task 단위에서 사용되는 upstream/downstream의 직관적인 사용 방법에 비해 주의해야할 점들이 몇 가지 존재한다. 이 글에서는 이러한 점들에 대해 설명하고 예시 코드를 더해 복붙에 용이한 형태로 기록을 남겨두려고 한다.


구성

아래와 같이 두 개의 DAG - [DAG_A, DAG_B]를 구성한다. DAG_A의 동작이 끝나는 Task_2가 완료되었을 때 DAG_B를 시작하여 Task_3을 실행하는 구성이다.

  • DAG_A
    • 이 경우 기존 DAG_A 구성에는 추가할 코드가 없다.
  • DAG_B
    • ExternalTaskSensor Operator를 사용하여 DAG_A를 sensing한다.
    • sensor는 task의 일종으로 sensing 이후 실행할 task를 downstream 설정하여 연결한다.

Example

  • DAG_A

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    
      from datetime import datetime
    
      from airflow import DAG
      from airflow.operators.dummy_operator import DummyOperator
      from airflow.operators.python_operator import PythonOperator
    
      def print_execution_date(ds):
          print(ds)
    
      default_args = {
          'owner': 'airflow',
          'depends_on_past': False,
          'start_date': datetime(2020, 11, 30),
      }
    
      ds = ''
    
      with DAG(
          dag_id='DAG_A', 
          schedule_interval='0 0 * * *',
          default_args=default_args) as dag:
    
          task_1 = DummyOperator(
              dag=dag,
              task_id='Task_1'
          )
          task_2 = PythonOperator(
              dag=dag,
              task_id='Task_2',
              python_callable=print_execution_date,
              op_kwargs={'ds': ds},
          )
          task_1 >> task_2
    
    • DAG_A 의 코드 구성은 외부의 DAG_B와 아무런 관련이 없다.
    • Task_1, Task_2 순서로 실행되며 Task_2의 끝에 execution_date을 로그에 남기는 것을 끝으로 작업을 마무리한다.
    • 작업은 매일 한번, 0시에 실행하도록 설정했다. (한국 시간 기준으로 9시)
  • DAG_B

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    
      from datetime import datetime, timedelta
    
      from airflow import DAG
      from airflow.operators.sensors import ExternalTaskSensor
      from airflow.operators.python_operator import PythonOperator
    
      def print_execution_date(ds):
          print(ds)
    
      ds = ''
      start_date = datetime(2020, 11, 30)
    
      default_args = {
          'owner': 'airflow',
          'depends_on_past': False,
          'start_date': start_date
      }
    
      with DAG(
          dag_id='DAG_B', 
          schedule_interval='0 0 * * *',
          default_args=default_args) as dag:
    
          sensor = ExternalTaskSensor(
              task_id='wait_for_task_2',
              external_dag_id='DAG_A',
              external_task_id='Task_2',
              start_date=start_date,
              execution_date_fn=lambda x: x,
              mode='reschedule',
              timeout=3600,
          )
    
          task_3 = PythonOperator(
              dag=dag,
              task_id='Task_3',
              python_callable=print_execution_date,
              op_kwargs={'ds': ds},
          )
    
          sensor >> task_3
    
    • DAG_B도 DAG_A와 동일한 매일 0시 실행되도록 스케쥴을 지정했다.
    • ExternalTaskSensor 설정은,
      • external_dag_id
        • 예시의 DAG에서는 DAG_A 이다.
      • external_task_id
        • 예시의 코드에서는 Task_2 가 종료된 이후 실행되도록 설정하였다.
      • start_date
        • DAG과 무관하게 시작일자를 지정할 수 있다. DAG보다 시작일자가 뒤에 있는 경우, 앞선 job은 failed 상태로 skip된다.
      • execution_date_fn
        • sensing 대상의 execution_date을 입력한다. 예시의 경우(lambda x: x) execution_date 그대로 입력한 셈이다. 즉, DAG_A가 실행된 execution_date에 매치되는 execution_date에 DAG_B가 실행되어야 할 경우를 위한 예시이다.
      • mode=’reschedule’
        • DAG_A의 Task_2가 완료될 때까지 up_for_reschedule 상태로 대기한다. 일정 시간마다 DAG_A의 Task_2의 상태를 확인하게 된다.
      • timeout
        • up_for_reschedule 상태를 3,600초간 유지한다. 그 이후에는 failed 상태가 된다.
  • DAG_A, DAG_B 등록 이후

    1. DAG_A 실행 → 실행 완료 large
    2. DAG_B → up_for_reschedule 상태 large 이 상태에서 일정 시간마다 DAG_A 모니터링을 시작한다.
    3. DAG_B → DAG_A의 Task_2가 종료된 이후 queued → running → success 진행 large 모두 성공한 상태가 되었다.
    4. DAG_B 성공 이후 로그 large …Poking for DAG_A.Task_2.. 설정한 id들이 적절했는지 여기서 확인한다.

주의사항

  • execution_delta or execution_date_fn
    • 실행 시간의 delta 혹은 실행 일자의 지정. 두가지 sensing methods가 제공된다.
    • 둘 중 하나의 파라미터만 입력해야 한다.
  • external_dag_id and external_task_id naming
    • DAG의 ID와 코드에서의 task variable의 이름이 항상 동일하게 이름 지어진다면 혼란의 여지가 없지만 그렇지 않다면 파라미터 입력 시에 주의해야 한다.

      1
      2
      3
      4
      5
      
        # ex
        task_1 = DummyOperator(
            dag=dag,
            task_id='Task_1'
        )
      
      • 위 예에서는 task_1에 task_id Task_1 인 DummyOperator를 생성해 할당했다.
      • tT 대소문자 한글자 차이에 따라 아이디가 잘못 입력될 경우 DAG은 up_for_reschedule을 timeout 시간만큼 유지하다가 failed 상태가 된다.
  • Parent DAG clear
    • DAG_A의 clear 이벤트는 DAG_B에 어떤 영향도 주지 않는다. 즉, DAG_A를 clear하는 경우 DAG_B도 clear 해야 한다.
    • 이 경우를 위해서는 DAG_A에 ExternalTaskMarker Operator를 사용해야 한다.

      1
      2
      3
      
        parent_task = ExternalTaskMarker(task_id="parent_task",
                                         external_dag_id="example_external_task_marker_child",
                                         external_task_id="child_task1")
      
      • 위와 같이 parent DAG에 child DAG의 정보를 입력해 downstream 연결하듯 사용한다.

References


Google Analytics GA4 101

달라진 점 + 주요 기능과 개념 + 설정 방법

Google Analytics 4 속성은 베타 버전이었던 앱+웹 속성의 정식 명칭으로 2020년 10월 14일 이후 모든 새 속성의 기본 환경이자 유일한 속성이다. 대부분의 구현 환경은 유지되나 보고서 기능의 변화와 함께 데이터 수집, 보관과 관련된 새로운 기능을 활용할 수 있게 된다. 이 글에서는 새로운 속성, GA4에서 달라진 점, 주요 기능과 개념, 생성 및 설정 방법에 대해 설명한다.


달라진 점

  • 보고서
    • 메뉴 구성이 달라졌다. 보고서의 종류와 구성이 매우 달라졌다.
    • GA 360 사용자에게만 제공되었던 분석 허브가 제공된다.
  • 이벤트와 사용자 속성
    • 자동으로 수집하는 이벤트가 추가된다.
      • session_start, user_engagement, first_visit 등의 이벤트가 수집된다. 상세한 이벤트 목록은 여기에서 확인할 수 있다.
      • 웹, 앱에 따라 자동으로 수집되는 이벤트 항목이 다르다.
      • gtag.js를 사용하면 별도의 설정없이 수집을 시작한다.
    • 향상된 측정 기능이 제공된다.
      • [페이지 조회, 스크롤, 이탈 클릭, 사이트 검색, 동영상 시청, 파일 다운로드]가 자동으로 측정된다. 이 기능은 별도의 설정으로 켜고 끌 수도 있다.
      • 페이지 조회 이벤트는 페이지 로드 이외에도 브라우저 방문 기록 이벤트를 추적하여 기록한다. Single Page Application 등에서 pushState를 사용하여 URL을 변경하는 경우에서도 추적이 가능해진다. (이 기능 또한 해제가 가능하다.)
    • 추천 이벤트 기능이 제공된다.
      • 이벤트 이름과 파라미터 이름에 선점된 키워드가 존재하고, 그에 맞춰 이벤트 로그를 남길 경우 그에 맞춰 특별히 제작된 보고서를 볼 수 있게 된다.
    • Event Name + Parameters 로 이벤트를 전달하게 되었다.

      1
      2
      3
      4
      5
      
        // As-is 
        ga('send', 'event', [eventCategory], [eventAction], [eventLabel], [eventValue]);
      
        // To-be
        gtag('event', 'login', {'key1': 'value1', 'key2': 'value2'});
      
      • Firebase의 그것과 동일한 구성으로 변경된 셈이다. 기존의 [Category, Action, Label, Value]과 같이 배열 객체가 아니라 이벤트의 이름과 파라미터 오브젝트로 구성하여 이벤트를 기록한다.
      • 입력 파라미터의 제한 또한 Firebase의 그것과 동일하다.
        • 고유 이벤트 이름 500개, 이벤트당 파라미터의 수 25개, 파라미터 이름 길이 40글자, 파라미터 값 길이 100글자 (영문 기준인 점을 주의해야 한다.)
    • 사용자 속성 이 추가되었다.
      • 이벤트와 마찬가지로 사용자 속성 또한 Firebase와 동일한 제한을 가진다.
        • 속성 당 사용자 속성 25개, 이름의 길이 24글자, 값의 길이 36글자
      • 기존의 Custom dimension을 대체하는 개념이다.
  • 개선된 데이터 제어
    • 데이터 수집 기능이 개선되었다.
      • 구글 신호 데이터 (교차 기기 잠재고객) 수집 설정, 광고 개인 최적화 지역 제어, 사용자 데이터 수집 확인 등을 설정할 수 있다.
    • 데이터 보관
      • 집계 데이터가 아닌 사용자 수준의 데이터의 보관 기간을 설정할 수 있다. [2개월, 14개월] 중 선택할 수 있다.
    • 데이터 필터
      • 뷰 단위가 아닌 속성 단위에서 데이터 필터를 적용하도록 변경되었다.
      • 개발자 트래픽 혹은 내부 트래픽으로 구분하여 필터링 할 수 있다.
      • 데이터를 제거하거나, 식별자를 추가 할 수 있다.

주요 기능과 개념

  • 속성

    The level of the Analytics account that determines which data is organized and stored together.

    • 속성은 다수 스트림을 가질 수 있다. 따라서 다수 앱이나 다수 웹페이지의 기록을 수집하고 저장할 수 있다.
    • 스트림 설정과 앱/웹과의 연결 없이는 아무런 데이터도 수집되지 않는다.
  • 데이터 스트림

    The flow of data from an app or website to your Google Analytics 4 property.

    • 데이터 스트림을 통해 앱, 웹 등과 직접적으로 연결하여 정보를 수집하게 된다.
    • 웹사이트에 설치한 gtag 혹은 태그매니저가 스트림에 데이터를 전달하게 된다.
    • 각 스트림마다 태그 설정이 가능하다. (이벤트의 수정이나 추가, 내부 트래픽의 정의)
    • iOS, Android, Web 스트림 연결 제공
    • 각 스트림마다 측정 프로토콜 API 비밀번호 관리가 가능하다.
      • POST Request를 통해 이벤트를 기록할 수 있다. 이 때 API 비밀번호를 사용한다.
  • gtag.js
    • 로그 수집을 위한 자바스크립트 라이브러리. 수집 대상 웹페이지에 항상 삽입되어야 한다.
    • 코드 스니펫이 실행되는 즉시 페이지 조회 등의 자동 수집 이벤트 기록이 남기 시작한다.
      • React 등을 사용하는 SPA에서 pushState 등의 URL change event 없이는 인식되지 않고, 페이지 조회 기록이 누락된다.
  • 이벤트

    Data that represents a user interaction that your site or app sends to Google Analytics.

    • 가장 작은 단위의 기록이다.
    • gtag 코드를 삽입하여 실행한다.

      1
      
        gtag('event', 'login', {'key1': 'value1', 'key2': 'value2'});
      
  • 사용자 속성

    An attribute of a user, such as age, gender, language, and country.

    • 사용자 수준의 기록이다.
    • 이벤트의 발생과 무관하게 기록 이후의 모든 레코드에 고정된다. (새로운 값으로 업데이트하기 전까지)

필수 설정

  1. 계정과 속성 생성
    • 가장 먼저 Google Analytics 계정 생성부터 진행한다. large 계정이 없는 경우 첫 페이지 large 계정 이름 설정
    • 속성을 생성한다. large
  2. 스트림 생성
    • 속성에 연결할 스트림을 생성한다. large
    • 스트림의 URL, 이름을 지정한 뒤 생성을 완료한다.
  3. 태그 추가
    • 생성한 스트림에 스크립트 삽입을 위한 코드 스니펫이 제공된다. 해당 코드를 웹사이트의 해더에 삽입한다.

      1
      2
      3
      4
      5
      6
      7
      8
      
        <!-- Global site tag (gtag.js) - Google Analytics -->
        <script async src="https://www.googletagmanager.com/gtag/js?id=G-0000000000"></script>
        <script>
          window.dataLayer = window.dataLayer || [];
          function gtag(){dataLayer.push(arguments);}
          gtag('js', new Date());
          gtag('config', 'G-0000000000');
        </script>
      
      • 실제로 이 스크립트는 태그매니저의 그것과 거의 동일하다. 새로운 스크립트만으로도 태그매니저 미리보기 기능에 적용되어 이벤트 디버깅이 가능하다.
      • 내부 구현은 analytic.js를 사용한다. gtag는 analytic 제품과 함께 구글의 다른 제품들 - tagmanger, Ads 등의 wrapper library이다. 이러한 제품들과 연동하지 않는다면 gtag.js의 60kb를 로드하는 데에 더 많은 시간과 자원을 사용하게 되는 셈이다.
  4. 테스트 스크립트 삽입 이후에 태깅이 잘 설정되었는지 확인하는 방법(디버깅 방법)이 다수 지원된다. 필수적으로는 웹사이트 헤더에 스크립트 삽입이 정상적으로 되었는지 확인하는 것이 필요하고 이후 개발자 도구에서 상세한 설정값들과 이벤트 발생 기록 등을 확인할 수 있고, 디버그뷰 보고서를 통해 실제 Google Analytics 제품에 정보가 전달되는지와 어떤 정보가 전달되었는지 검수할 수 있다.
    1. 스크립트 실행 확인
      • 배포 이후 브라우저에서 웹페이지로 이동하여 스크립트가 정상적으로 삽입되었는지 확인한다.
      • 웹 사이트에 스크립트 삽입을 확인한다. 크롬 브라우저 → 검사 → html 영역에서 삽입된 스크립트가 헤더 영역에 있는지 확인한다. large
    2. 콘솔 출력 확인
      • 디버그 도구를 설치한다.
      • 디버그 모드를 활성화한다. (클릭하면 On/Off 토글된다.) small
      • 활성화 이후 콘솔을 확인하면 GA에 전달되고 있는 모든 정보들을 확인할 수 있다. large
    3. 디버그뷰 보고서 확인
      • 메뉴 → DebigView 메뉴로 이동한다. 현재 디버그 모드가 활성화된 기기의 이벤트 스트림을 확인할 수 있다. large
      • 각 파라미터를 선택하여 실제 입력한 매개변수의 값을 확인할 수 있다.
    4. 실시간 보고서 확인
      • 메뉴 → 실시간 메뉴로 이동한다. 실시간 개요 페이지에서 현재 접속 중인 사용자 전체를 확인할 수 있다. large

부가 설정

  • 빅쿼리 연동

    빅쿼리와 연동 시 원본 데이터에 직접 접근이 가능해진다. 즉, Google Analytics의 보고서뿐만 아니라 다른 BI 도구나 분석용 도구들 - jupyter, datastudio, tableau 등으로 데이터를 옮겨 활용하는 것이 가능해진다.

    • 속성별 차이
      • Firebase와 연동한 속성, 일반 혹은 신규 속성의 경우 설정 가능한 메뉴가 상이하다.
      • Firebase에서 속성 생성한 경우 GA 관리 도구에서 빅쿼리 연동하는 대신, Firebase console → settings → integration 에서 설정한다. (Exported Integration 항목에 GA와 거의 동일한 설정이 있다.
      • 그 외의 경우 GA → 관리 → BigQuery 연결에서 설정한다.
    • 설정 순서 및 항목
      1. GA Property 관리 페이지로 이동 → BigQuery 연결 선택
      2. BigQuery 프로젝트와 지역 설정
        • 데이터세트의 이름까지 지정할 수 없다. Google의 naming rule에 따라 자동 생성된다.
      3. 스트림 설정
        • 앞서 설정해둔 스트림 중 빅쿼리 연동할 스트림을 선택해야 한다.
      4. 광고 식별자 포함 여부
        • 광고 식별자 - ADID, IDFA, IDFV에 대한 기록 여부를 설정한다.
      5. 빈도 설정
        • 매일 : 매일 한번씩 새로운 테이블을 생성한다. 필수적으로 선택되어야 한다.
        • 스트리밍 : 실시간 내보내기 기능이 활성화 된다. 스트리밍 입력의 비용이 별도로 부과되므로 주의가 필요하다. (Bigquery streaming insert pricing을 따른다.)
    • 주의사항
      • 속성당 하나의 프로젝트에만 연결이 가능하다.
      • 2020-12-07 이후 Realtime Streaming Insert 설정이 기본 활성화된다. 빅쿼리의 가격정책을 따라 과금된다. (전달할 이벤트 파라미터의 길이와 무관하게 하나의 레코드가 1kb의 최소 사이즈를 가지는 것에 주의해야 한다. 즉, 파라미터가 비어있는 이벤트를 보낸다 해도 하나의 이벤트당 1kb로 계산된다.)
      • Firebase Project와 연동된 속성이라면, Firebase project의 요금제 설정이 종량제여야 한다. 무료 요금제를 사용중이라면 빅쿼리 연동을 설정할 수 없다.
      • 즉시 데이터 입력이 진행되지 않는다. 하루 정도 기다려야 한다.

        연결이 완료되면 24시간 이내에 데이터가 BigQuery 프로젝트로 전송되기 시작합니다. 일일 내보내기를 사용 설정하면 전날의 데이터가 포함된 1개의 파일(일반적으로 보고서에 설정한 시간대의 이른 오후)이 매일 내보내기됩니다.

  • 데이터 필터

    내부 트래픽 혹은 테스트 과정에서 입력된 데이터를 식별할 수 있도록 식별자를 추가하거나 기록에서 제외시킬 수 있다.

    • 설정 순서 및 항목
      • 내부 트래픽 설정

        속성 수준에서 설정된 IP를 기준으로 내부 트래픽을 구분한다. 따라서 설정해둔 스트림보다 상위 수준에서 설정하고, 적용된다.

        1. GA Property 관리 페이지로 이동 → Data Stream 선택
        2. Additional Settings → More Tagging settings 선택
        3. Define internal traffic → Create 클릭, 설정 입력 후 생성
          • traffic_type : 내부 트래픽을 제외하지 않고 테스트로 설정하는 경우, 매개변수 키 이름이다. 입력한 문자열이 매개변수의 값으로 입력된다.
          • IP주소 : IP 주소를 기준으로 필터링 설정한다. IP 일치/시작/끝/포함/범위 조건 설정이 가능하다.
      • Data Filter 설정

        1. GA Property 관리 페이지로 이동 → Data Settings → Data Filters 선택
        2. Create Filter 클릭 → 용도에 따라 [Developer Traffic, Internal Traffic] 선택
        3. 설정 입력 후 생성
          • 필터 연산 : 필터 조건에 해당하는 경우 제외하거나 포함시킬 수 있다.
          • traffic_type : 필터 상태를 테스트로 설정하는 경우 추가될 식별자의 값을 설정한다.
          • 필터 상태 : 테스트로 설정하는 경우 별도의 식별자를 붙이게 되고, 활성으로 설정하는 경우 기록에서 제외된다.
    • 주의사항
      • 설정한 필터 상태가 활성으로 설정되는 경우, 데이터가 기록에서 제외된다. 어디에도 데이터가 보관되지 않게 된다.
      • 필터 적용 이후부터 적용 된다. 소급 적용되지 않는다.

References


github pages + jekyll로 블로그 제작하기

github pages + jekyll (+ cloud9)

블로그 제작을 쉽고 빠르게 하면서도 입맛에 맞는 기능을 갖추기 위해 github pages, jekyll, cloud9을 사용했다. google 검색 결과 노출 후 유입에 최적화하고 유입 이후의 읽기 외 기능을 모두 간결하게 정리하는 것이 주요한 요구사항이었으며, 부가적으로 글쓰기/배포 환경의 편의를 위해 로컬 작업환경이 아닌 온라인 인스턴스에 Cloud9 IDE를 구성해 추가하였다. 추후 신규 웹사이트 생성 혹은 반복 작업이 이뤄질 때를 위하여 빠르게 따라할 수 있도록 작업 과정을 순서대로 나열하였다.


작업 진행

테마 검색

Cloud9 설정 (Optional)

  1. AWS Console 접속 → cloud9 검색, 이동 medium
  2. AWS Cloud9 인스턴스 생성 클릭 medium Cloud9 첫 화면

    medium Cloud9 생성 시 설정들

    • Environment type : 접속 환경 설정
    • Instance type : 머신 성능 설정
    • 플랫폼 : 운영체제 설정
    • Cost Saving setting : 일정 시간 사용하지 않는 경우 동작을 멈추도록 설정한다.

    medium 생성 완료된 IDE

    medium cloud9 접속 중인 화면

    medium cloud9 첫 화면

    • 첫 화면 이후 Tools → Terminal 선택하여 터미털 창을 실행한다.

Jekyll 설치

  1. Ruby install
    • Installing Ruby 웹페이지 방문, 운영체제에 맞춰 링크 선택
      1
      2
      
       // Cloud9 기본 설정의 경우 CentOS
       sudo yum install ruby
      
    • 루비 설치 완료 확인
      1
      2
      
       ruby -v
       // 결과로 "ruby 2.6.3p62 (2019-04-16 revision 67580) [x86_64-linux]" 출력되면 정상
      
  2. Jekyll install

Theme 설치

  1. Theme download
    • lanyon theme 사용
      1
      
       git clone https://github.com/poole/lanyon
      
  2. Plugin install
    • 테마가 요구하는 플러그인을 먼저 설치
      1
      
       bundle install
      

Start Editing

  1. jekyll 시작
    1
    
     jekyll serve --port 8080
    
    • 기본 포트는 3000, cloud9 외부 접속을 위한 포트는 제한적 → 8080으로 여는 경우 즉시 연결 가능한 URL이 제공된다.
      1
      
       jekyll server --livereload
      
    • 소스 변경 시 자동 업데이트, cloud9 기본 설정 상태에서 사용 불가.
  2. Config 수정
    • 기본 테마의 설정들을 모두 수정한다.
    • Google Analytics messurement id 등이 기본값으로 등록되어 있다.
  3. header 수정
    • 헤더 파일에 기본적인 스크립트와 스타일, Google analytics 설정이 포함되어 있다. (경로는 → /_includes/head.html)
    • 몇 가지 스타일 수정을 위해 custom.css 파일을 생성하고 헤더에 삽입한다.
    1
    
     <link rel="stylesheet" href="/public/css/custom.css">
    
    • 기본 설정의 경우 gtag.js 가 아닌 ga.js로 기본 설정되어 있다. → gtag 스타일로 변경한다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
     <!--as-is-->
     <script>
         (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
         (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
         m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
         })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
         ga('create', '{{ site.google_analytics_id }}', 'auto');
         ga('send', 'pageview');
     </script>
    
     <!-- to-be -->
     <script>
     window.dataLayer = window.dataLayer || [];
     function gtag(){dataLayer.push(arguments);}
     gtag('js', new Date());
        
     gtag('config', '{{ site.google_analytics_id }}');
     </script>
    
    • 기존 스크립트와 동일하게 {{ site.google_analytics_id }} < 트래킹 아이디를 별도의 변수로 추가한다. 해당 변수는 /_config.yml 파일에서 수정한다.

Github Pages

  1. 저장소 생성
    • github repository 생성 → 링크 medium

    • repository의 이름이 사용자이름.github.io 으로 생성되어야 한다. (필수)

  2. git push
    • gh-pages 브랜치를 생성한다.
    • origin에 push한다.
  3. github setting
    • github 저장소 → Setting → GitHub Pages → Source → branch gh-pages 선택
  4. 잠시 대기
    • push 직후 빌드가 진행된다.
    • 잠시 기다리면 제작한 웹사이트가 000.github.io에 띄워진 것을 확인할 수 있다.