UX 테크놀로지스트의 생존기 in CRO and Analytics: 당신의 데이터태깅 플랜이 먹히지 않는 이유
특히 UX테크놀로지스트로 살아가며 타 직군과 부대끼다 보면, 여러모로 내 생각을 어떻게 설득시키며 전달시킬 지 더욱 절감하게 된다. 그들은 종종 자신들이 아는 것만 이야기할 뿐, 프론트엔드 생태계에 대한 이해 부족이 어떻게 비즈니스의 발목을 잡는지에 대해서는 놀라울 만큼 무관심하다.
Analytics 팀이 던져준 ‘AI 코드’, 왜 작동하지 않는가?
데이터 분석이나 마케팅 테크 분야 동료들과의 협업은 종종 기묘한 좌절감을 안겨준다. 특히 요즘 유행하는 LLM, Gen AI를 활용해 코드를 짜서 넘겨주는 경우가 그렇다. “이 스크립트만 심으면 유저 행동 추적이 될 겁니다.” 같은 말을 아무렇지 않게 하곤 한다. 심지어 “아, 이 캠페인은 당신의 도움이 필요 없어요. 챗GPT에서 코드를 생성해서 넣었거든요.” 라며 자기들끼리 일을 처리해서 나중에 프로덕션 단계까지 이미 Window Time은 다 소진했다.
하지만 실제 상황에서 그 코드가 제대로 박혀 돌아가는 경우는 거의 없다. 왜겠는가? 그들은 프론트엔드 스택에서 브라우저가 어떻게 렌더링 최적화를 하는지, 비동기적으로 데이터가 어떻게 흘러가는지에 대한 이해가 전무하기 때문이다.
간단히 말해, 그들의 머릿속 세상은 이렇다.
graph TD
A[웹사이트 로딩] --> B{스크립트 실행};
B --> C[데이터 수집! 🎉];
하지만 실제 프론트엔드의 세상은 훨씬 복잡한 지옥도다.
graph TD
A[HTML 파싱 시작] --> B(DOM 트리 생성);
B --> C{CSSOM 트리 생성};
C --> D[렌더 트리 구축];
D --> E{레이아웃 계산};
E --> F[페인팅];
G[비동기 스크립트 로드] --> H{이벤트 루프 대기};
H --> I[DOM 조작];
F & I --> J[**사용자 인터랙션 가능 시점**];
K(데이터 팀 스크립트) --> L{DOM에서 특정 요소 탐색};
J --> L;
L -- 요소 못 찾음 --> M[**에러 발생 or 무한 대기**];
얼마 남지 않은 시간만 남겨두곤 디버깅하라고 종용한다. 기계 생성된 코드에 어떠한 코멘트도 없어 대체 뭔 목적으로 이런 코드를 쓴 건지 이해도 안 가고 결국 얼마 남지 않은 시간 동안 머리를 쥐어짜며 새로 쓰는게 일상 다반사이다.
클라이언트에게 요구사항을 받아올 때도 마찬가지다. 프로덕트의 아키텍처나 데이터 흐름에 대한 이해 없이 ‘이 버튼 눌렀을 때 이 데이터가 잡혀야 해요’ 같은 요구만 덜렁 가져온다. 우리가 구축한 가상돔(Virtual DOM) 위에서 상태(State)가 어떻게 변하고, 그에 따라 어떤 요소가 언제 렌더링되는지 알 턱이 없으니, 적용 불가능한 요구를 너무나도 쉽게 하는 것이다.
픽셀 퍼펙트의 망령과 싸우는 법
UX 디자이너와의 협업이라고 크게 다르진 않다. 물론 시각적 완성도를 추구하는 그들의 장인 정신은 존중한다. 하지만 동적 레이어, 반응형 그리드 시스템, 컴포넌트 기반 아키텍처에 대한 이해 없이 ‘1픽셀도 틀리지 않게’를 외치는 건, 마치 자동차 엔진의 원리는 무시한 채 겉모습만 똑같이 찰흙으로 빚으라는 소리와 같다.
그들은 정적인 디자인 파일(Figma, Sketch 등) 안에서 세상을 본다. 모든 것이 고정되어 있고, 예측 가능하며, 완벽하게 통제된다. 그러나 실제 웹 환경은 살아있는 유기체와 같다. 사용자의 화면 크기, 기기의 종류, 데이터의 유무와 길이에 따라 모든 것이 변한다.
디자이너: “여기 간격이 왜 2px 더 벌어져 있죠? 피그마랑 다른데요.”
개발자: (속마음) “그건 화면 너비가 달라지면서 그리드 시스템에 의해 자동으로 계산된 값이고, 저 텍스트가 두 줄이 될 가능성은 생각 안 해봤는가 말이지…”
이런 상황이 반복되면 개발자는 디자이너의 ‘픽셀 집착’에 대한 방어막을 치게 되고, 결국 생산적인 논의 대신 서로의 입장만 되풀이하는 소모적인 관계로 전락하고 만다.
기술 부채보다 무서운 ‘소통 부채’
Stakeholder. 기획자, PM, 때로는 C레벨 임원까지. 그들은 종종 아이디어라는 구름 위에서 노닐면서 그들은 멋진 보물섬 지도를 들고 와서 말한다. “이 지도대로 배를 만들어 보물을 가져와라.” 하지만 그들이 들고 온 지도는 지구상에 존재하지 않는 섬을 그리고 있다는 사실은 아무도 말해주지 않는다.
논리적 비약? 기술적 제약? 그런 것들은 ‘실무자의 의지 부족’ 혹은 ‘역량 부족’이라는 편리한 단어로 치환될 뿐이다. 그들은 기술을 마치 필요할 때마다 원하는 결과물을 뚝딱 만들어내는 마법 지팡이쯤으로 여긴다.
이런 대화, 지겹도록 겪어보지 않았는가?
Stakeholder: “사용자의 감성을 실시간으로 분석해서 배경색이 바뀌는 기능을 넣어주세요. 경쟁사엔 없어서 우리만의 킬러 기능이 될 겁니다.”
나: “웹캠으로 사용자의 표정을 분석하는 건 기술적으로 매우 복잡하고, 사용자 프라이버시 문제와 성능 저하가 심각할 겁니다. 현실적으로 어렵습니다.”
Stakeholder: “어렵다는 말 말고요. 방법을 찾아야죠. 개발자가 왜 있겠어요? 의지가 부족한 거 아닙니까?”
왜 이런 일이 반복되는가? 그들은 ‘결과의 분리(Separation of Consequence)’ 라는 특권을 누리기 때문이다. 아이디어를 던지는 사람은 그 아이디어가 초래할 기술 부채, 유지보수 비용, 연쇄적인 버그의 파도에 대해 책임지지 않는다. 그 책임은 온전히 키보드 위에서 밤을 새우는 개발자의 몫으로 남는다.
graph TD
subgraph "Product 'Tower of Babel'"
PM("기획: '하늘에 닿게!'")
Designer("디자인: '아름답게!'")
Frontend("프론트엔드: '이 벽돌은 규격에 안 맞고, 저쪽은 무너질 것 같은데...'")
Backend("백엔드: '기초가 부실한데...'")
end
PM -- "요구사항" --> Designer
Designer -- "완성된 시안" --> Frontend
Frontend -- "API 요청" --> Backend
Backend -- "데이터 구조 문제 지적" --> Frontend
Frontend -- "기술적 제약 설명" --> PM & Designer
PM & Designer -- "왜 안돼요? 그냥 하세요" --> Frontend
subgraph "결과"
Bugs("버그와 기술부채 덩어리")
end
Frontend --> Bugs
이런 상황이 반복되면 개발자는 냉소주의라는 갑옷을 입게 된다. 어차피 논리적인 설득은 통하지 않을 것이고, 그들의 요구는 언젠가 문제를 일으킬 것이라는 사실을 체념하듯 받아들이게 된다. 결국 무너지는 것은 언제나 현장에서 돌을 쌓아 올리던 이들의 몫이지 않겠는가. 말이지.
이것은 현대판 바벨탑을 쌓는 것과 같다. 각자 다른 언어(기획의 언어, 디자인의 언어, 개발의 언어)를 쓰면서 하나의 건축물을 올리려 하니, 제대로 된 탑이 세워질 리가 있겠는가.
가장 힘든 건, 비개발 직군 동료가 개발 환경 구축 과정에 불쑥 끼어들 때다. 심지어 챗GPT에게 코드 리팩토링을 시키라는 조언을 들었을 땐 실소를 금할 수 없었다. 이건 단순히 무지를 넘어, 전문성에 대한 존중이 없다는 명백한 신호다.
이런 상황에서 나는 어떻게 반응해야 하는가? 3년 가까이 데이터 기반을 쌓으며 모아온 사용자 경험의 중요성을 역설하며 끈질기게 설득해봤지만, 아무도 듣지 않았다. 그들은 결국 자신들의 목표, 즉 비즈니스 성과에만 관심이 있다.
렌더링 로직은 전혀 고려치 않고 제시되는 요구사항
웹 분석가나 CRO(전환율 최적화) 담당자들의 문제는 더욱 노골적이다. 그들의 지상과제는 오직 하나, ‘데이터 태깅’이다. 사용자의 모든 클릭, 스크롤, 페이지 이동을 추적하기. 문제는 그들이 브라우저가 실제로 어떻게 웹페이지를 그리는지에 대한 이해가 전혀 없다는 점이다.
그들의 머릿속에서 웹페이지는 한 번에 ‘짠’하고 나타나는 정적인 그림과 같다. 하지만 현대 웹 애플리케이션, 특히 SPA(Single Page Application)는 그렇지 않다. 사용자와의 상호작용에 따라 필요한 부분만 동적으로 다시 렌더링된다. 페이지 전체를 새로고침하지 않기 때문에, 전통적인 방식의 페이지뷰 이벤트는 최초 한 번만 발생할 뿐이다.
이런 기술적 배경에 대한 이해 없이, 그들은 GTM(Google Tag Manager) 같은 툴을 맹신하며 불가능한 요구를 던진다.
graph TD
subgraph "분석가의 이상적인 세계"
A[사용자 클릭] --> B{GTM이 마법처럼 감지};
B --> C[데이터 수집 성공!];
end
subgraph "프론트엔드 개발자의 현실 (SPA)"
D[최초 페이지 로딩] --> E[초기 렌더링, GTM 로드];
F[사용자, 다른 메뉴 클릭] --> G["URL 변경 (History API)"];
G --> H{컴포넌트 비동기 로드 & 재렌더링};
I(GTM) --"페이지 새로고침이 없네?"--> J[페이지뷰 이벤트 감지 못함 ❌];
H --> K[**분석가가 원하는 버튼은 아직 DOM에 없음**];
L(분석가의 태깅 스크립트) --"버튼 어딨어?"--> M[**null 에러 발생**];
end
이 간극을 메우기 위해 “버튼이 나타날 때까지 기다렸다가 태그를 심어주세요” 같은 임시방편적인 요구가 들어온다. 이는 결국 성능 저하와 잠재적인 메모리 누수를 유발하는 ‘나쁜 코드’를 양산하게 만든다. 전환율을 높이기 위해 도입한 CRO 툴이 오히려 사이트 속도를 저하시켜 이탈률을 높이는 코미디는 바로 이런 무지에서 시작된다.
결국, 지도에 없는 섬을 그리며 ‘데이터’를 외치는 디자이너와, ‘데이터 태깅’이라는 깃발만 꽂으려는 분석가 사이에서, 개발자는 난파선의 선장이 되어 홀로 파도와 싸우는 꼴이다. 그들은 자신의 이론과 케이스 스터디, 대시보드 속 숫자만 볼 뿐, 그 모든 것이 구현되는 ‘브라우저’라는 현실의 바다는 보려 하지 않는다. 말이지.
예를 들어, CRO(전환율 최적화)나 데이터 분석 문제의 근원은 대부분 프론트엔드 이슈에서 비롯된다는 사실을 모르는 경우가 태반이다. 느린 페이지 로딩, 복잡한 사용자 인터페이스, 접근성 문제 등은 모두 전환율을 깎아 먹는 주범이다. 이걸 설명을 어떻게 해줘야할지 참 막막하다. 대화란 한쪽이 아우성을 친다고해서 되진 않으니 말이다. 상대가 들을 맘이 없는데 어떻게 뭘 설득을 하란 말인가.
분석가의 장부와 개발자의 코드, 그 처절한 동상이몽
데이터 분석가나 전략가가 가져오는 요구사항 정의서(Requirement Definition)는 종종 한 폭의 잘 그린 정물화 같다. 모든 것이 제자리에 고정되어 있고, 명확하며, 질서정연하다. “이 로고를 클릭하면 ‘home_logo_click’ 이벤트가 한 번 잡히게 해주세요.” 얼마나 간단하고 명료한 요구인가.
하지만 그들이 보고 있는 것이 정물화일 때, 우리가 다루는 것은 시시각각 형태를 바꾸는 동적 컨베이어 벨트와 같다. 그들은 소스코드를 보지 않는다. 그 결과, 그들은 애플리케이션의 ‘겉모습’을 기준으로 ‘동작’을 정의하는 근본적인 오류를 범한다.
최근에 실제로 겪었던 사례를 보자.
분석가: “모바일과 데스크탑 화면에서 보이는 메인 로고를 클릭하면, 구글 애널리틱스(GA)와 어도비 애널리틱스(AA) 양쪽에 ‘home_logo_click’ 이벤트가 정확히 1개씩만 전달되어야 합니다.”
이 요구는 너무나도 합리적으로 들린다. 하지만 프로덕트의 리액트 앱 소스코드를 열어보면, 현실은 전혀 다르다.
// components/Header.js
import { useViewport } from '../hooks/useViewport';
import MobileLogo from './MobileLogo';
import DesktopLogo from './DesktopLogo';
function Header() {
const { isMobile } = useViewport(); // 현재 뷰포트가 모바일인지 확인하는 커스텀 훅
return (
<header>
{/* ... 다른 헤더 요소들 ... */}
{ isMobile ? <MobileLogo /> : <DesktopLogo /> }
{/* ... 다른 헤더 요소들 ... */}
</header>
);
}
분석가의 눈에는 그저 하나의 ‘로고’이지만, 코드상에서는 화면 너비에 따라 MobileLogo와 DesktopLogo라는 두 개의 서로 다른 컴포넌트가 조건부 렌더링(Conditional Rendering)되고 있다. 사용자가 브라우저 창 크기를 조절하면, 기존 컴포넌트는 DOM에서 사라지고(Unmount) 새로운 컴포넌트가 그 자리를 차지한다(Mount).
이 구조를 모르는 분석가는 GTM 같은 툴에서 간단한 클릭 트리거 하나로 모든 것을 해결하려 든다. 하지만 실제로는 두 컴포넌트에 각각 이벤트 핸들러를 붙여야 하고, 이 과정에서 문제가 발생하기 시작한다. 만약 두 컴포넌트에 각각 동일한 이벤트 태깅 코드를 심는다면?
// components/MobileLogo.js
function MobileLogo() {
const handleClick = () => {
// 구글 애널리틱스와 어도비 애널리틱스로 이벤트 전송
window.dataLayer.push({ event: 'home_logo_click' });
window.s.t({ eVar1: 'home_logo_click' });
};
return <img src="/logo-m.svg" onClick={handleClick} alt="Logo" />;
}
// components/DesktopLogo.js
function DesktopLogo() {
// Ctrl+C, Ctrl+V... 비극의 서막
const handleClick = () => {
window.dataLayer.push({ event: 'home_logo_click' });
window.s.t({ eVar1: 'home_logo_click' });
};
return <img src="/logo-d.svg" onClick={handleClick} alt="Logo" />;
}
이 코드는 대부분의 경우 잘 동작하는 것처럼 ‘보인다’. 하지만 특정 상황, 예를 들어 페이지가 로드되고 리액트가 어떤 이유로든 이 Header 컴포넌트를 짧은 순간에 다시 렌더링(re-render)해야 할 때, 이벤트가 중복으로 집계되는 악몽이 시작된다. 분석가의 대시보드에는 ‘클릭 수 2회’라는 유령 데이터가 찍히고, 원인을 찾으라는 압박은 고스란히 개발자에게 돌아온다.
실전 예제: 흔한 태깅 코드의 함정
최근에 겪은 일이다. 특정 상품 페이지에서 스크롤을 내리면 ‘Add to Cart’ 버튼이 화면에 고정되는 스티키(Sticky) 버튼을 구현하는 태스크였다. 분석 팀에서 “이 스크립트만 넣으면 됩니다”라며 코드를 던져줬다.
// 대략 이런 느낌의 코드였다.
function waitForElement(selector, callback) {
const interval = setInterval(() => {
const element = document.querySelector(selector);
if (element) {
clearInterval(interval);
callback(element);
}
}, 100);
}
function init() {
// 특정 버튼이 나타나면, 복제해서 스티키 버튼을 만들어라!
waitForElement('[data-analytics-pdp="add-to-cart"]', (element) => {
// ...버튼을 복제하고 스타일을 입히는 로직...
});
}
init();
겉보기엔 간단하다. 특정 셀렉터를 가진 요소가 나타날 때까지 기다렸다가, 나타나면 콜백 함수를 실행하는 코드.
하지만 이 코드는 실제 환경에서 제대로 동작하지 않았다.
왜?
서비스의 상품 정보는 페이지 로딩 후 비동기 API 호출을 통해 가져와 렌더링되기 때문이다. 분석 팀의 스크립트가 실행되는 시점엔 [data-analytics-pdp="add-to-cart"] 버튼이 아직 DOM에 존재하지 않았던 것이다.
결국 코드를 이렇게 수정해야만 했다. DOMContentLoaded로 DOM 로딩을 기다리고, waitForElement 함수에 타임아웃과 폴백 로직을 추가했다. 단순히 ‘기다리는’ 것을 넘어, ‘얼마나’, ‘어떻게’ 기다릴지, 그리고 ‘실패했을 때’는 어떻게 처리할지를 모두 고려해야 하는 것이다.
function injectStyles() {
// ... (스타일 주입 로직) ...
}
function renderFunction(element, addToCart) {
// ... (스티키 버튼 렌더링 및 스크롤 이벤트 처리) ...
}
function waitForElement(selector, callback, timeout = 8000, fallback = null) {
let maxCount = timeout;
var checkExist = setInterval(() => {
maxCount -= 10;
var element = document.querySelector(selector);
if (element != null) {
clearInterval(checkExist);
callback(element);
} else if (maxCount <= 0) { // 타임아웃 처리
clearInterval(checkExist);
if (fallback != null) fallback(); // 실패 시 폴백 함수 실행
}
}, 10);
}
function init() {
console.log("injector testing-278");
injectStyles();
// DOM이 준비된 후에 스크립트를 실행하도록 보장
document.addEventListener("DOMContentLoaded", function () {
waitForElement(
'form > div > section:nth-child(5) > div > div:nth-child(3) > [data-analytics-pdp="add-to-cart"]',
function (element) {
// ... (성공 로직) ...
document.body.style.visibility = "visible";
console.log("he:sticky button loaded");
},
8000, // 8초의 타임아웃
function () { // 폴백 함수
console.log("he:fallback:sticky button failed");
document.body.style.visibility = "visible"; // 실패해도 화면은 보여줘야 한다
}
);
});
}
init();
이런 과정은 밖에서 보기엔 그저 ‘버튼 하나 다는 일’처럼 보일지 모른다. 하지만 그 안에는 비동기 처리, DOM 렌더링 시점, 예외 처리 등 수많은 복잡성이 숨어있다. 프론트엔드 개발은 결코 간단하지 않다. 진입 장벽이 낮아 보일 뿐, 잘하기는 더 어려운 분야라는 것을, 그들도 언젠가는 이해하게 될까.
더 최악의 사례는 리액트 18의 StrictMode 환경에서 useEffect 훅을 사용할 때다.
분석가: “특정 프로모션 컴포넌트가 화면에 보여졌을 때 ‘promotion_viewed’ 이벤트를 한 번만 보내주세요.”
개발자는 자연스럽게 useEffect를 사용한다.
useEffect(() => {
// 컴포넌트가 렌더링되면 '보여졌다'고 간주하고 이벤트 전송
analytics.track('promotion_viewed');
}, []); // 최초 한 번만 실행되도록 빈 배열을 넘긴다. 완벽해!
하지만 개발 환경(StrictMode)에서 리액트는 의도적으로 컴포넌트를 마운트 -> 언마운트 -> 다시 마운트 시킨다. 잠재적인 버그를 잡기 위한 리액트의 친절한 배려다. 그 결과는? ‘promotion_viewed’ 이벤트가 두 번 전송된다.
graph LR
subgraph "개발 환경 (React 18 StrictMode)"
A[컴포넌트 마운트] --> B["useEffect 실행 (이벤트 1회 전송)"];
B --> C[컴포넌트 언마운트];
C --> D[컴포넌트 재마운트];
D --> E["useEffect 다시 실행 (이벤트 2회 전송)"];
end
subgraph "분석가의 대시보드"
F{왜 이벤트가 2번씩 찍히죠? 😨}
end
E --> F
분석가는 QA 과정에서 이 현상을 보고하며 “코드를 수정해달라”고 요구한다. 개발자는 이것이 개발 환경의 특성이라고 설명해야 하지만, 결국 오해를 피하기 위해 useRef 같은 훅으로 플래그를 만들어 이벤트를 한 번만 실행시키는 방어 코드를 추가하게 된다. 본질적인 로직이 아니라, ‘데이터 태깅’ 요구사항을 맞춰주기 위한 기형적인 코드가 탄생하는 순간이다.
결국 그들의 깔끔한 대시보드와 정갈한 보고서 뒤에는, 이처럼 수많은 예외 처리와 방어 코드로 누더기가 된 컴포넌트가 묵묵히 돌아가고 있을 뿐이다. 그들은 소스코드를 보지 않고 현상을 진단하고, 우리는 그들의 진단에 맞춰 코드를 수술해야 한다. 이 얼마나 비효율적이고 소모적인 연금술인가. 말이지.
결국 이 모든 건 ‘소통’의 문제로 귀결된다. 내 전문성이 제대로 평가받지 못하고, 심지어 일부 남성 관리자들에게는 성과를 평가절하당하는 경험까지 겪다 보면 자괴감이 들 때도 있다.
하지만 나는 전문가로서 문제를 정의하고, 원인을 분석하며, 해결책을 찾는 사람이다. 복잡한 기술 문제를 풀어내듯, 이 꼬여버린 소통의 매듭도 하나씩 풀어가야 하지 않겠는가. 내 전문성을 그들의 언어로 번역하고, 나의 기여가 비즈니스에 어떤 실질적 가치를 더하는지 어떻게 설명해줘야할지 그것이 가장 큰 챌린지이다.