이벤트 QA 자동화 회고 — 개발편

Jiyoon You
원티드랩 기술 블로그
12 min readMar 20, 2023

안녕하세요! 원티드랩 데이터팀에서 데이터분석을 하고 있는 유지윤입니다 🙂

최근에 저희 팀의 Data Quality & Governanance Manager (DQGM) 인 최자연님과 진행한 이벤트 QA 자동화 프로젝트에 대한 회고글을 공유드리려고 합니다.

🎯 하게된 이유

원티드엔 6개의 크고 작은 스쿼드가 있고, 짧게는 2주에 한번 배포가 이루어지고 있어요. 분석에 필요한 데이터에 대한 전반은 데이터팀에서 관리해요. 원티드는 대부분의 행동 데이터를 앰플리튜드로 분석하고, 구글 태그 매니저 (GTM)으로 이를 관리하고 있어요. 팀에서 심은 특정 행동 (클릭, 페이지뷰)가 발생했을 때 “이벤트가 발생했다”라고 얘기해요. 이벤트를 심는 과정은 아래와 같아요.

  1. 이벤트 리스트업 — PO/분석가
  2. 네이밍 컨벤션 확인 — PO/분석가 ->DQGM
  3. 개발 요청 — 분석가/DQGM -> 개발자
  4. 개발환경에 심어진 이벤트 확인 — 분석가/DQGM
  5. GTM/앰플리튜드 설명 추가 — 분석가/DQGM

이처럼 한 배포에 대해서 여러 단계의 작업이 필요하고, 스쿼드와 프로젝트의 수가 늘어남에 따라 이 작업에 투자하는 시간 또한 늘어났어요. 자연스럽게 자동화할 수 있는 부분이 어디일지 고민을 해보았고, 4.개발환경에서 심어진 것을 확인하는 이벤트 QA 작업이 코드를 통해 자동화가 가능한 부분이라 생각했어요.

💭 어려웠던 점 (1) — 검증 범위를 어떻게 정해야할까?

이벤트 QA는 요청한 이벤트가 발생하는 케이스를 개발환경에서 직접 조작한 후 이벤트가 발생하는 것을 앰플리튜드를 통해 확인하는 일이예요. 즉, 기존엔 End-to-End 테스트 방식으로 검증을 진행했었어요. 하지만 이와 같은 방식을 코드로 구현했을 때 이슈가 발생하면 디버깅이 어려워지는 단점이 있어요. 예를 들어, 아래에 표시된 것과 같이 유저 행동과 이벤트 발생만 확인했을 때, 중간 과정인 트리거에서 이슈가 발생한다면 버그를 찾기 위해 확인하는 과정이 추가로 필요할 거예요.

위와 같이 이슈가 발생했을 때 발생 위치와 원인을 정확하게 파악하기 위해서 Unit Test를 할 수 있어요. Unit Test는 논리적으로 독립되는 최소한의 단위를 각각 검증하는 방식이예요. 주로 코드에 쓰이지만 이벤트 검증 과정에서 이를 적용한다면, 각각의 단계에서 검증을 진행하는 방식이 되지 않을까 생각했어요.

그리고 GTM에서 태그가 정상적으로 발생했을 경우 자동으로 발생하는 앰플리튜드 이벤트는 검증 대상에서 제외했어요.

요약하자면,

  • 개발자가 작업하는 원티드data layer, custom event 검증
  • DQGM이 작업하는 GTM 트리거 태그 검증

이 두가지를 검증 범위로 정하고, 코드를 작성했어요.

GTM의 트리거 유형 중 아래 3가지를 원티드에서 주로 사용하고 있어요.

  • 클릭 트리거: data attribute라고 하는 html 태그에 추가하는 data- 형태의 커스텀한 attribute를 심으며, 이는 태그 매니저의 맞춤 HTML 태그를 사용해 자동으로 감지해 앰플리튜드 이벤트로 발생시키는 dispatcher를 쓰고 있어요.
  • 맞춤 이벤트: 웹페이지에서 데이터를 담고 있는 요소인 data layer의 맞춤 이벤트가 트리거되면 data layer로 함께 전달된 값을 설정하고 태그를 발생시켜요
  • 페이지 조회 트리거: 싱글 페이지가 아닌 경우 별도의 작업 없이 사용할 수 있는 트리거지만, 싱글 페이지인 원티드는 page__view 라고 하는 같은 이름의 data layer를 페이지가 로드될 때 발생시켜, 페이지 조회 트리거와 같은 의미로 쓰고 있어요.

💭 어려웠던 점 (2) — 인풋을 어떤 형태로 받는 것이 최적일까?

원티드에선 어떤 이벤트를 심을지에 대한 내용을 구글시트 (이하 “요청 시트”) 로 관리하고 있어요. 이 시트를 그대로 인풋으로 받아 코드에서 활용하기에는 아래와 같은 이슈가 있었어요

시트의 서식이 일정하지 않았어요.

이벤트를 리스트업하는 단계에서는 PO와 피드백을 주고받고, 컨벤션을 확인하는 단계에선 DQGM과 피드백을 주고받아요. 또한 이 이벤트들을 개발자에게 지라를 요청한 이후에도 변경사항이 생길 수 있어요. 이 모든 과정에서 사람은 시트의 서식 (글씨색, 배경색, 취소선, 굵게)을 사용해서 눈으로 수정이 이루어진 것과 진짜로 요청한 것이 무엇인지 구분을 할 수 있지만, 코드로는 이 과정이 어려워요.

이벤트 검증에 필요하지 않은 정보가 시트에 혼재되어 있었어요.

이벤트 검증에 필요한 “이벤트 리스트”의 시작하는 지점과 끝나는 지점이 앞뒤의 내용에 따라 달라질 수 있기 때문에 해당 정보만 추출하기 까다로워요.

그래서 검증에 용이한 형태로 가져올 수 있도록 요청 시트의 구성에 대한 수정을 진행하게 됐어요.

(전) 이벤트 리스트를 포함한 모든 정보가 한 페이지에 적힌 형태 (후) 이벤트 리스트만 있는 별도의 시트 생성

자연님의 고민과 논의 끝에 (후) 스크린샷처럼 이벤트 리스트만 한 시트에 담는 방식으로 요청 시트 수정을 진행했어요. 그리고 서식에 대한 변화는 최소화하고, 문의사항은 댓글로 진행하는 등 깔끔하게 시트를 유지하기로 저희만의 룰을 정했어요.

마지막으로, 기존에는 없었지만 검증에 반드시 필요한 정보, 예를 들어,

  • 이벤트가 발생하는 페이지의 URL
  • 트리거 유형에 따라 각 이벤트의 유형

을 요청시트에 포함시켜 코드 실행시 필요한 정보를 모두 요청 시트에서 가져올 수 있게끔 수정했어요.

🤝 구현 방법

위와 같은 고민들을 한 결과, 최종적인 코드 구조는 아래와 같이 정리했어요.

  1. 인풋으로 요청 시트를 가져온다.
  2. 요청 시트에 적힌 이벤트가 해당 URL에서 data layer, data attribute 로 잘 발생하는지 확인한다.
  3. 요청 시트에 적힌 이벤트가 GTM에서 태그/트리거 규칙에 맞게 생성되었는지 확인한다.

순서대로 살펴보면,

1. 인풋으로 요청 시트를 가져온다.

요청 시트의 주요 컬럼은 아래와 같아요.

  • Event Name: 이벤트명
  • Event Type: page view, custom event, data attribute
  • URL: 이벤트가 발생하는 페이지 URL
#로컬에서 가져오려면
df = pd.read_csv('요청시트.csv')

참고로, 원티드에서는 구글 시트로 요청 시트를 작성했기 때문에 구글 드라이브에서 요청시트를 가져오는 방식으로 코드를 작성했어요.

2. 요청 시트에 적힌 이벤트가 해당 URL에서 발생하는 data layer, data attribute 를 가져온다.

특정 url의 data layer와 data attribute을 가져오는 함수를 아래와 같이 작성했어요.

추후에 특정 액션 (ex. 로그인) 을 한 이후 data layer와 data attribute을 수집할 가능성이 있을거라 생각해서 웹사이트의 요소와 상호작용이 가능한 selenium으로 웹페이지에 접속해서 수집하는 방식을 택했어요.

위 함수들은 마지막 코드처럼 호출하면, test_events와 test_data_attribute 변수에 각각 data layer와 data attribute가 불러와집니다.

def scrape_data_layer(driver, url):
driver.get(url)
# 데이터 레이어 추출
datalayers = driver.execute_script("return dataLayer;")
filter_layers = [dl.get('event') for dl in datalayers if dl.get('event') not in ['optimize.activate', 'gtm.js', 'gtm.dom', 'gtm.load']]
return filter_layers
def scrape_data_attribute(driver, url):
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

driver.get(url)

WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, '[data-attribute-id],[data-attribute-id] *')))
tags = driver.find_elements(By.CSS_SELECTOR, '[data-attribute-id]')

attributes = [tags[i].get_attribute('data-attribute-id') for i in range(len(tags))]

return attributes
def init():
from seleniumwire import webdriver
from webdriver_manager.chrome import ChromeDriverManager

chrome_options = webdriver.ChromeOptions()
# 필요에 따라 옵션을 추가해주세요
# https://www.selenium.dev/documentation/webdriver/browsers/chrome/#options
driver = webdriver.Chrome(ChromeDriverManager().install(), chrome_options=chrome_options)

return driver
driver = init()
url = 'https://wanted.co.kr'
test_events = scrape_data_layer(driver, url)
test_data_attribute = scrape_data_attribute(driver, url)

3. 요청 시트에 적힌 이벤트가 GTM에서 태그/트리거 규칙에 맞게 생성되었는지 확인한다.

# base와 compare 비교하여 compare 중 base에 없는 item이 있다면 프린트
def compare_result(test_name, base, compare):
test_result = set(base) - set(compare)
if test_result:
print(f"[ ] {test_name}")
print(test_result)
else:
print(f"[v] {test_name}")

참고로, GTM 태그/트리거 같은 경우 아래와 같이 수집했어요.

def scrape_tagmanger():
service = service_accounts('tagmanager', 'v2')
# 아래 세 변수는 GTM에서 찾아서 입력해주세요
account = 1234567890
container = 12345689
workspace = 12

# list tags
triggers = service.accounts().containers().workspaces().triggers().list(
parent=f'accounts/{account}/containers/{container}/workspaces/{workspace}').execute()
tags = service.accounts().containers().workspaces().tags().list(
parent=f'accounts/{account}/containers/{container}/workspaces/{workspace}').execute()
return triggers | tags

🌱 실행 결과

복잡하게 설명했지만 결국 이벤트QA를 통해 하고자하는 것은 간단해요.

  • 인풋: 요청시트 (=요청한 & 확인할 이벤트리스트)
  • 아웃풋: 인풋의 이벤트들과 웹에서 수집한 결과를 비교하여 수정이 필요한 항목 확인

그리고 수정을 한 후 코드를 재실행시키는 방법으로 더블체크도 가능해졌어요.

소중한 첫 발견의 순간 ✨

🌈 마치며

위에서 설명한 코드는 처음에 언급한 개발환경에 심어진 이벤트 확인을 모두 완벽하게 하진 못해요. 왜냐하면 이벤트가 발생하는 케이스는 다양하고, 모든 케이스를 세세하게 코드로 구현하는 것 또한 작업 공수가 들기 때문이에요.

하지만 전체 중 일부지만 가장 중요한 부분을 먼저 작업을 했고, 이 회고가 끝이 아니라 앞으로 더 확장시킬 수 있는 작업이라는 생각이 들어요.

2차 회고로 다시 오길 바라며 글을 마칠게요 👋 읽어주셔서 감사합니다!

--

--