Do not index
Do not index
0. 개발 계기0.1 user_id가 안들어오는데요?0.2 기존 QA 방법1. 어떻게 해결해야 할까요?1.1 요구사항 정리하기1.2 기존에 존재하는 GA4 DebugView 활용하기2. 앱 내 이벤트 로그 시스템 개발하기2.1 개발 과정2.2 개발 결과2.3 피드백 반영하기2.3.1 웹뷰 이벤트 핸들링2.3.2 사용성 개선하기후기References
안녕하세요. 오토피디아에서 프론트엔드 개발을 맡고 있는 유서경입니다. 저는 제품 개발도 좋아하지만 실제 사용자가 남긴 데이터에 흥미가 많은 편입니다. 닥터차 서비스에서는 Google Analytics 4를 사용하여 사용자 로그를 수집하여 더 나은 사용자 경험을 만들기 위해 노력 중이며, 이벤트를 설계하고 구현하는 과정은 매우 중요합니다. 이번 글에서는 이벤트가 잘 심어졌는지 확인하는 QA 단계를 개선하기 위해 시스템을 구상하고 개발한 이야기를 담았습니다. 사내에서 어떤 불편함이 있었는지 그리고 어떠한 고민 끝에 배포 환경에 관계없이 앱 내에서 실시간으로 이벤트 로깅 모달을 개발하게 되었는지, 저의 미니 프로젝트를 재미있게 설명드리겠습니다.
0. 개발 계기
0.1 user_id가 안들어오는데요?
최근 제가 개발한 자동차세 환급 프로젝트는 웹, 앱 내 WebView로 탑재된 서비스이며 회원가입 이벤트가 굉장히 중요한 지표입니다. 해당 프로젝트가 출시된 이후 데이터 엔지니어님이 저장된 이벤트를 확인 하시던 중 사용자를 특정할 수 있는 고유값이 없는 경우가 있다고 말씀해주셨습니다. 이벤트 QA를 출시 전에 잘 마무리했는데 왜 이런 일이 발생한 것일까요?
0.2 기존 QA 방법
우리의 서비스에 태깅된 이벤트가 Google Analytics 4(GA4)에 저장되기 위해서는 앱은 Fireabase, 웹은 Tag Manager를 거쳐야 합니다. 닥터차에서는 앱은 전송하는 이벤트를 토스트 팝업을 통해 웹에서는 Tag Manager의 미리보기 기능을 활용하였고, 통상적으로 이벤트 구현은 프론트엔드 개발자, QA는 데이터 엔지니어가 진행하고 있었습니다.
이번 프로젝트의 이벤트 QA에서는 두 가지 문제가 있었습니다.
- 앱에서 웹뷰의 이벤트를 확인할 수 없어 QA를 하지 못하였다.
- GA4에는 이벤트(이벤트명, 이벤트 파라미터)와 사용자 속성이 함께 저장되는데, 사용자 속성을 QA하지 못하였다.
1. 어떻게 해결해야 할까요?
문제를 발견하였으니 해결할 차례입니다. 먼저 이벤트 QA 당사자인 데이터 엔지니어님과 상의하여 아래와 같은 요구사항을 정리하였습니다. 이번 저의 미니 프로젝트에서는 테크스펙을 작성하여 빠른 기간 내 필요를 우선적으로 해결하려 노력했습니다.
1.1 요구사항 정리하기
No. | 문제 사항 |
P1 | GA에 저장되는 사용자 속성을 확인할 수 없음 |
P2 | 앱의 토스트 팝업: 이벤트 파라미터가 많으면 확인이 어려움 |
P3 | 앱의 토스트 팝업: 단시간에 여러 이벤트가 발생하면 토스트가 발생하지 않음 |
P4 | 앱의 토스트 팝업: 단일 이벤트를 실시간으로만 확인할 수 있어 전체 로그들의 파악이 어려움 |
P5 | 앱과 웹의 QA 플랫폼이 달라 번거로움 |
이번 미니 프로젝트의 요구사항을 중요도 순으로 정리해보았습니다. 웹의 Tag Manager는 비교적 잘 구축된 서비스로 문제가 적었지만, 앱은 자체적으로 토스트 팝업만 발생시키므로 불편한 점이 많았습니다. 예시로 아래 영상과 같이 이벤트 파라미터가 5개나 되는 경우 짧은 시간 내 눈으로만 올바르게 구현되어있는지 확인하기가 어려운 것이 사실입니다.
1.2 기존에 존재하는 GA4 DebugView 활용하기
이 많은 문제를 한 번에 해결할 수 있는 방법이 있을까요? 바로 GA4에 저장되는 데이터를 직접 확인하는 것입니다. GA4에서는 DebugView라는 기능을 지원하고 웹에서는 Google Analytics Debugger 크롬 익스텐션을 켜기만 하면 쉽게 활용할 수 있습니다. React Native 앱에서는 iOS는 Xcode 상에서 Scheme에
-FIRDebugEnable
flag를 추가하고, Android는 특정 커맨드를 실행하여 비교적 쉽게 디버그 모드를 켤 수 있습니다. 그러나 이 서비스를 도입하는 것에 두 가지 문제점이 있었고 도입을 철회하게 되었습니다.
- 데이터 엔지니어가 프론트엔드 개발 환경을 구축하여야 함
- 앱에서는 debugView를 켜기 위해 터미널 상에서 앱을 빌드해야 했고, 개발 환경 구축이 꽤 까다로운 React Native를 세팅하기 시간이 걸릴 것이라 판단했습니다.
- 또한 코드 저장소 관리 및 브랜치 설명, QA마다 발생하는 소통 비용이 클 것으로 예상했습니다.
- 기기 인식이 불안정하고 user_id가 변경되면 새로운 기기가 생성되는 버그 존재
- 아래는 앱을 열고 사용자 속성이 변경될 때 기기 연결이 끊긴 모습입니다. 앱과 웹 모두 기기 인식이 딜레이가 있거나 불안정한 모습을 보였습니다.
2. 앱 내 이벤트 로그 시스템 개발하기
그렇다면 이 문제를 단기간 내에 어떻게 해결할 수 있을까요? 저는 우선적으로 문제가 많이 발생하는 앱 내 자체 구현 시스템을 고도화한 후 P5의 문제는 추후 해결하는 것으로 결정하였습니다. 현재 앱과 같은 토스트 팝업과 함께 이벤트 로그를 확인할 수 있는 모달을 개발하여 배포 환경에 관계없이 P1부터 P4의 문제점을 해결하는 시스템을 개발하기로 하였습니다.
No. | 문제 사항 |
P1 | GA에 저장되는 사용자 속성을 확인할 수 없음 |
P2 | 앱의 토스트 팝업: 이벤트 파라미터가 많으면 확인이 어려움 |
P3 | 앱의 토스트 팝업: 단시간에 여러 이벤트가 발생하면 토스트가 발생하지 않음 |
P4 | 앱의 토스트 팝업: 단일 이벤트를 실시간으로만 확인할 수 있어 전체 로그들의 파악이 어려움 |
2.1 개발 과정
앱 내 어디서든 플로팅 버튼이 보여져야 하고, 모달 컴포넌트와 외부 이벤트 전송 함수 간 데이터 공유가 일어나야 하므로 Redux를 도입하였습니다. 이벤트 확인 용도로 도입하는 라이브러리이므로 툴킷과 같은 추가 라이브러리없이
redux
react-redux
만 추가하였습니다.먼저 상태가 공유되는 store를 만들고 이벤트 데이터에 관련된 함수들을 만든 후 상위 컴포넌트인 App에서 Provider로 감쌉니다.(ChatGPT의 도움을 받았습니다 ^^)
Redux 관련 코드
// store.ts
import { createStore, combineReducers } from 'redux';
import { eventReducer } from './eventQaReducer';
export const rootReducer = combineReducers({
eventLog: eventReducer,
});
export type RootState = ReturnType<typeof rootReducer>;
export const store = createStore(rootReducer);
// reducer.ts
import {
ADD_EVENT_QA,
EventActionTypes,
CLEAR_EVENT_QA,
EventQaType,
} from './eventQaActions';
const initialState: EventQaType[] = [];
export const eventReducer = (
state = initialState,
action: EventActionTypes,
) => {
switch (action.type) {
case ADD_EVENT_QA:
return [...state, action.payload];
case CLEAR_EVENT_QA:
return [];
default:
return state;
}
};
// actions.ts
export const ADD_EVENT_QA = 'ADD_EVENT_QA';
export const CLEAR_EVENT_QA = 'CLEAR_EVENT_QA';
export type EventQaType = {
name: string;
params: any;
time: string;
gaUser: string | null;
};
export interface AddEventQaAction {
type: typeof ADD_EVENT_QA;
payload: EventQaType;
}
export interface ClearEventQaAction {
type: typeof CLEAR_EVENT_QA;
}
export type EventActionTypes = AddEventQaAction | ClearEventQaAction;
export const addEventQa = (event: EventQaType): EventActionTypes => {
return { type: ADD_EVENT_QA, payload: event };
};
export const clearEventQA = (): ClearEventQaAction => {
return { type: CLEAR_EVENT_QA };
};
const App = () => {
...
return (
<ApolloProvider client={client}>
<Provider store={store}>
<EventQaButtonAndModal />
...
</Provider>
</ApolloProvider>
);
};
이벤트를 전송하는
sendEvent
함수에서 GA4에 이벤트를 전송할 때 함께 sendEventQaModal
를 호출하여 모달 컴포넌트에 데이터를 전송합니다./**
* 이벤트 debug 모드일 경우 EventQaModal에 이벤트를 전송하는 함수
* @param name 이벤트명
* @param params 이벤트 파라미터
*/
export const sendEventQaModal = async (name: string, params: any) => {{
const gaUser = await AsyncStorage.getItem('gaUser');
store.dispatch(
addEventQa({
name,
params,
time: dayjs().format('HH:mm:ss'),
gaUser,
}),
);
};
/**
* 일반 이벤트 로그 함수
* @param event 이벤트 이름(특수 이벤트일 경우에 조회안됨)
* @param params 해당 이벤트에 대응되는 변수 object
*/
const sendEvent = async <T extends keyof Events>(
event: T,
params?: Events[T],
) => {
sendAdjustEvent(adjustToken[event], params);
await analytics().logEvent(event, params);
renderToast(event, { ...params });
sendEventQaModal(event, params);
};
모달 컴포넌트에서 해당 이벤트를 잘 그려주면 개발이 완료됩니다.
const EventQaButtonAndModal: React.FC = () => {
const eventLogs = useSelector((state: RootState) => state.eventLog);
return (
<>
<FloatingButton
horizontalPosition={horizontalPosition}
verticalPosition={verticalPosition}
/>
<Modal
animationType="slide"
visible={isModal}
onRequestClose={onModalClose}
>
<ModalContainer>
..
<ModalContents>
<Events>
{eventLogs.length === 0 && (
<Typography.Paragraph size={2} color={Colors.GRAY_900}>
기록된 이벤트가 없습니다.
</Typography.Paragraph>
)}
{eventLogs.map((eventLog, index) => (
<EventLog
key={`${eventLog.time}${eventLog.name}`}
eventLog={eventLog}
index={index}
/>
))}
</Events>
</ModalContents>
</ModalContainer>
</Modal>
</>
);
};
export default EventQaButtonAndModal;
2.2 개발 결과
GA4 DebugView와 유사하게 모달 컴포넌트에서는 이벤트 발생 시각, 이름, 파라미터, 사용자 속성을 제공합니다. 고도화 전과 비교하자면 파라미터가 많은 이벤트여도 로깅 모달에서 간편하게 확인할 수 있습니다. 또한 이전에 발생한 이벤트도 이제 디버깅이 가능해졌습니다.
2.3 피드백 반영하기
QA 시스템 초안이 완료된 후 이 모달을 가장 많이 사용하실 데이터 엔지니어님께 피드백을 요청하였습니다. 결과물을 상당히 마음에 들어하셨고 두 가지 보완점을 말씀해주셨습니다.
2.3.1 웹뷰 이벤트 핸들링
앱 내 웹뷰에서 이벤트가 발생하면 이 이벤트는 앱이 아닌 웹 이벤트이므로 앱 내에서 QA가 어렵습니다. 따라서 웹에서
console.log
로 메시지를 전송하면 앱에서 이 데이터를 활용하도록 웹뷰 컴포넌트에 아래와 같은 로직을 추가하였습니다. <WebView
...
onMessage={({ nativeEvent }) => {
if (nativeEvent.data.includes('gtmEventData')) {
const firstSplit = nativeEvent.data.split(' gtmEventData: ');
const eventName = firstSplit[0].split('Console log: ')[1].trim();
const eventParameters = firstSplit[1].trim() || '';
sendEventQaModal(`🌐${eventName}`, JSON.parse(eventParameters));
}
}}
/>
아래 좌측 영상을 확인하면 웹뷰에 대해서도 이벤트가 발생하고, 이벤트가 거의 동시에 발생하여 토스트 팝업이 뜨지 않은 경우에도 모달에서 잘 확인이 되고 있습니다.또한 앱 내 GA 사용자 속성은 웹의 GA 사용자 속성 무관하므로 혼란을 방지하기 위해 웹 이벤트 발생 시 사용자 속성은 보이지 않도록 하였습니다. 추후 웹의 사용자 속성도 따로 보여지면 더 좋은 시스템이 될 것 같습니다.
2.3.2 사용성 개선하기
플로팅 버튼을 우측 상단에 배치하다보니 버튼이 겹치는 경우 앱 사용에 불편함이 발생하는 경우가 있었습니다. 따라서 이벤트 플로팅 버튼을 각 모서리로 이동할 수 있도록 하는 UX를 추가하였습니다. 또한, 해당 모달 관련 정보를 깃허브 PR로 저장해두기보다는 모두가 확인할 수 있도록 information 툴팁을 추가하여 안내 사항을 적어두었습니다.
후기
1주라는 단기간 내에 미니 프로젝트를 재미있게 잘 진행한 것 같습니다. 아직 새로운 프로젝트에 이 시스템을 사용해보지는 않았지만, QA 과정에서 겪던 불편함을 잘 개선했다고 생각합니다. 토스트 팝업과 병렬적으로 이벤트 로그를 확인하면서 이벤트 그리고 사용자 속성까지 확인할 수 있는 점, 거의 동시에 이벤트가 발생하여도 로그를 파악할 수 있는 점이 가장 괄목할만한 점입니다. 제가 생각하는 개발자의 미덕 중 하나인 비효율성을 발견하고 직접 문제를 해결해보는 경험을 기획부터 개발까지 직접 해볼 수 있어 즐거웠습니다.