데이터 주도 개발을 위한 사용자 데이터 수집 시스템 구축 기록(feat. firebase analytics)

이승훈·2023년 11월 7일
2

시행착오

목록 보기
17/23
post-thumbnail

0. 들어가며

React Native FireBase Analytics 사용 준비 글에서 적어놓았듯 의견을 제시하고 기획을 제안할 때 데이터에 근거해 판단하는 문화가 도입되어야 한다고 생각했고 그 문화의 기반이 되는 데이터를 수집하기 위한 시스템을 구축하였다.
그 과정에 대한 회고 이다.

1. LogEvent 커스텀 훅 제작

import analytics from '@react-native-firebase/analytics';

analytics().logEvent('login', { method: 'email' });

위와같이 analytics메소드를 호출 및 실행 후 return된 logEvent함수를 실행함으로서 google firebase에 이벤트기록을 남길 수 있다.
허나 이걸 그대로 사용하기에는 문제점이 두가지 있었다.

  1. 로그 이벤트를 발생시킨 유저 정보를 담아야하는데 매번 유저정보를 호출해줘야한다.
  2. 로그 이벤트명이 스트링으로 관리됨으로서 같은 이벤트를 여러곳에서 사용할 때 오탈자의 여지가 있음.

위의 두가지 문제점을 해결하기 위해 logEvent를 위한 커스텀훅을 제작해주었다.
커스텀 훅 제작에 있어서 위의 두가지문제를 아래와 같이 해결해주었다.

  1. 커스텀훅은 실행시점에 유저정보를 가져와 logEvent함수에 저장하며 return해주는 logEvent함수는 기본으로 파라미터에 유저정보를 담고 있다.
  2. 이벤트는 별도의 상수로 관리하여 이벤트명을 중앙관리한다.

커스텀훅의 코드는 아래와같다.

import analytics from '@react-native-firebase/analytics';
import { Platform } from 'react-native';
import { useApolloClient } from '@apollo/client';
import { GetAccountUserQuery } from '@/graphql/auth/queries';
import { googleAnalyticsLogEventName } from '../constants/eventName/googleAnalyticsLogEventNames';

type ValuesOf<T> = T[keyof T];

const useGoogleAnalyticsLogRecord = () => {
  const { cache } = useApolloClient();
  const userInfoFromCache = cache.readQuery({
    query: GetAccountUserQuery,
  });

  const logEvent = async (
    eventName: ValuesOf<typeof googleAnalyticsLogEventName>,
    params?: Record<string, string | number>,
  ) => {
    try {
      analytics().logEvent(eventName, {
        ...params,
        OS: Platform.OS,
        userInfo_code: `${userInfoFromCache?.accountUser.code}`,
        userInfo_role: `${userInfoFromCache?.accountUser.role}`,
        userInfo_nickname: `${userInfoFromCache?.accountUser.userInfo.nicknameWithCode?.nickname}`,
        userInfo_nicknameCode: `${userInfoFromCache?.accountUser.userInfo.nicknameWithCode?.nicknameCode}`,
      });
    } catch (error) {
      console.error('Failed to log event: ', error);
    }
  };

  const logScreenView = ({ screenName }: { screenName: string }) => {
    analytics().logScreenView({
      screen_name: screenName,
      userInfo_code: `${userInfoFromCache?.accountUser.code}`,
      userInfo_role: `${userInfoFromCache?.accountUser.role}`,
      userInfo_nickname: `${userInfoFromCache?.accountUser.userInfo.nicknameWithCode?.nickname}`,
      userInfo_nicknameCode: `${userInfoFromCache?.accountUser.userInfo.nicknameWithCode?.nicknameCode}`,
    });
  };

  return {
    logEvent,
    logScreenView,
  };
};

export default useGoogleAnalyticsLogRecord;

유저정보를 가져올 때 처음엔 Get 요청을 보내 서버로부터 유저정보를 가져오는 방법을 생각하였으나
유저 정보는 처음 로그인 시 캐시에 저장되기 때문에 cache.readQuery를 통해 캐시로부터 유저정보를 가져오는 방법을 사용하여 불필요한 통신비용을 아낄 수 있었다.

위의 useGoogleAnalyticsLogRecord 커스텀훅은 고차함수로서 logEvent와 logScreenView 함수를 return해주며 아래와같이 필요 시 커스텀훅을 호출하여 return받은 함수를 사용할 수 있다.


import useGoogleAnalyticsLogRecord from '@/hooks/useGoogleAnalyticsLogRecord';
import { googleAnalyticsLogEventName } from '@/constants/eventName/googleAnalyticsLogEventNames';

const SomethingComponent = ({ setComments }: Props) => {
  const { logEvent } = useGoogleAnalyticsLogRecord();

  const onPressSomeButton = async () => {
      logEvent(googleAnalyticsLogEventName.community_write_comment, {
        param1:'param1',
        param2:'param2',
        .
        .
        .
  };

  return (
      <SomeButton onPress={onPressSomeButton} />
  );
};

export { SomethingComponent };

또한 이벤트명은 아래와같이 객체로 중앙관리하였으며 as const를 사용하여 readonly 속성을 부여하여 불의의 수정되는 사태를 방지하였습니다.

export const googleAnalyticsLogEventName = {
  event_name_1: 'event_name_1',
  event_name_2: 'event_name_2',
  event_name_3: 'event_name_3',
  event_name_4: 'event_name_4',
  .
  .
  .
  .
} as const;

2. 이벤트 기획 및 문서화

유저들의 사용패턴을 파악하기 위해선 최대한 많고 촘촘하게 데이터를 수집하는게 좋으나 이 업무에만 모든시간을 할애할 수 없기에 주요 이벤트들을 특정하였다.
주요 기능들의 버튼클릭, 제출, 화면진입들의 이벤트들 위주로 수집하였다.
또한 유저가 서버에 POST 요청을 날리는 이벤트가 발생될 때에는 보안에 문제가 되지 않는선에서 유저가 날린 요청사항들도 이벤트에 기록될 수 있게 기획 및 개발을 진행하였다.
이는 추후 유저들의 행동패턴 경향을 파악하는데 큰 도움이 될거라고 생각한다.

이벤트 수집을 코딩하는 작업은 하루만에 끝낼 수 없는 작업이기에 내가 어디까지 이벤트 기록을 달았는지,
또한 이 이벤트에는 어떤 파라미터를 담기러 하였는지,
현재까지 프로덕트에 적용된 이벤트 수집은 어디까지 되었는지를 잘 기록하고 공유하는것이 중요했다.

이벤트들의 전체 목록은 노션에 기록하였으며 중간중간 현황들은 피그마를 통해 공유하였다.

2-0. 노션 이벤트 리스트업 예시

2-1. 피그마 이벤트 기록

피그마에 접속 시 한눈에 이벤트 수집 현황을 파악할 수 있게 색을 달리 하여 이벤트들의 현황파악을 할 수 있게 하였다.

2-1-1. 작업 진행 중 피그마 모습

2-2-2. 작업 완료 후 피그마 모습

형광으로 가득찬 모습이... 너무 짜릿해..

3. 이벤트 수집 및 분석 기반 마련

이번 프로젝트를 시작할때의 최종목적은 다음과 같았다.

권한 있는 사람은 누구나 데이터를 쉽게 수집할 수 있게 하여 합리적 판단의 근거를 마련해 주자

위의 목표를 달성하기 위해선 쌓여진 데이터를 누구나 쉽게 수집, 접근할 수 있는 기반을 마련해주어야한다.
데이터에 접근하기 위한 방법은 2가지를 마련하였다.

  1. Firebase에 BigQuery를 연동하여 이벤트 RAW Data Table 출력
  2. Looker Studio를 사용하여 주요 데이터를 실시간 시각화

3-1. Firebase에 BigQuery를 연동하여 이벤트 RAW Data Table 출력

함께 SQL문을 작성해주신 GPT선생님께 감사인사 드립니다.

연결 메뉴얼은 이곳 설명에서 확인이 가능하다.
Firebase와 Bigquery의 연동은 메뉴얼도 잘 되어있고 함께 일하는 백엔드 개발자분이 도와주셔서 비교적 쉽게 완료할 수 있었다.

중요한건 데이터 테이블을 내가 원하는 양식으로 출력하는 과정이다.

빅쿼리는 SQL문을 사용하여 데이터를 출력한다.
일단 FE 개발자인 나도 SQL문에 익숙하지 않을뿐더러 비개발직군의 동료들 또한 SQL을 전혀 모르기에

알아서 잘 SQL문 입력해서 데이터 뽑아서 분석하세요.

라는 마무리는 할 수 없었다.

간단하게 SELECT, FROM, WHERE 에 대해서만 공부한 후 아래처럼 쿼리문을 작성 및 데이터를 출력해보았다.

의도와는 전혀 일치 하지 않는 출력결과를 확인하였다.
의도하는 출력된 데이터의 모양은 다음과 같다.

발생한 이벤트들이 시계열로 행단위로 나열되며
각각의 행에는 발생한 이벤트들에 대한 정보가 담겨져있는 데이터 테이블이 나의 의도이다.

일단 SELECT으로 추출한 event_params는 오브젝트이며 그 안에 각각의 이벤트들에 대한 정보를 담고 있다.
이 key-value 구조의 event_params를 일단 분리할 필요가 있었고 UNNEST를 사용하여 event_params데이터를 펴주었다.

SELECT 
  event_name, 
  event_date,
  app_info.version,
  param.key AS param_key,
  param.value.string_value,
  param.value.int_value,
  param.value.float_value,
  param.value.double_value
FROM 
  `ontol-poc-eeb84.analytics_354801107.events_*`,
  UNNEST(event_params) AS param
WHERE 
  event_date BETWEEN '20231018' AND '20231018'
  AND event_name NOT IN ('screen_view', 'user_engagement')

이 데이터를 잘 피보팅만 해주면 원하는 출력결과를 얻을 수 있었다.

COALESCE절을 통해 float, int, double, string 값 중 실제하는 값만을 param_value로 저장하였고 이렇게 UNNEST된 데이터를
WITH절을 사용하여 임시 테이블로 저장하였다.

이 후 임시 저장한 데이터에 대해 MAX(CASE WHEN ... THEN ... ELSE ... END):
이 패턴은 조건부 집계를 수행한다. 여기서 CASE 문을 사용하여 param_key 값에 따라 다른 param_value 값을 선택하고, 그 중 최대값(MAX)을 반환한다. 이 방식으로 하나의 행에 여러 param_key에 대한 값들을 모을 수 있습니다.

GROUP BY 절을 사용하여 event_name과 formatted_timestamp로 그룹화하면 하나의 event_name행에 여러 parma_key에 대한 값들을 모을 수 있다.

WITH unnested_params AS (
  SELECT 
    event_name,
    FORMAT_TIMESTAMP("%Y%m%d%H%M%S", TIMESTAMP_MICROS(event_timestamp)) AS formatted_timestamp,
    param.key AS param_key,
     COALESCE(
      CAST(param.value.int_value AS STRING),
      CAST(param.value.float_value AS STRING),
      CAST(param.value.double_value AS STRING),
      param.value.string_value
    ) AS param_value
  FROM 
    `ontol-poc-eeb84.analytics_354801107.events_*`,
    UNNEST(event_params) AS param
  WHERE 
    TIMESTAMP_MICROS(event_timestamp) BETWEEN TIMESTAMP("2023-10-14 00:00:00 UTC") AND TIMESTAMP("2023-10-23 23:59:59 UTC")
    AND event_name NOT IN ('screen_view', 'user_engagement')
)

SELECT 
  event_name,
  formatted_timestamp,
  MAX(CASE WHEN param_key = 'key1' THEN param_value ELSE NULL END) AS key1_value,
  MAX(CASE WHEN param_key = 'key2' THEN param_value ELSE NULL END) AS key2_value,
  MAX(CASE WHEN param_key = 'key3' THEN param_value ELSE NULL END) AS key3_value,
FROM
  unnested_params
GROUP BY
  event_name,
  formatted_timestamp
ORDER BY
  formatted_timestamp, event_name
  

위와같이 GPT선생님과 함께 쿼리문을 완성할 수 있엇다.
이제 이 쿼리문을 저장한 후 데이터가 필요한 사람은 아래의이미지의 기간에 해당하는 부분만 원하는 기간으로 변경만 하면 원하는 기간동안의 이벤트 데이터를 수집할 수 있게 되다.

추가적으로 누구든 이용할 수 있는 방법을 쉽게 알 수 있도록 메뉴얼을 별도로 작성하여 공유하였다.

메뉴얼 사진 일부 발췌

3-2. Looker Studio를 사용하여 주요 데이터를 실시간 시각화

데일리로 루틴하게 보는 지표들은 굳이 쿼리조회가 아니라 언제든 쉽게 조회할 수 있는 방법이 필요했다.
Looker Studio를 사용하여 DB 혹은 Firebase와 연동하면 실시간으로 쌓여가는 데이터를 원하는 형식으로
확인할 수 있다.

아래는 특정 데이터의 트렌드를 그린 차트이며 일정 주기로 실시간 업데이트가 된다.

4. 마무리

이제 사내 인원 누구든 유저들의 행동패턴 데이터에 대해 쉽게 접근할 수 있게되었다.
이를 기반으로 더 이상 의견을 결정할 때 누군가의 감이나, 혹은 느낌이 아닌 정확한 데이터를 기반으로 결정이 이루어졌으면 하는 바람이다.

.

profile
Beyond the wall

0개의 댓글