*이번 글에서 다루는 ‘자동화’ 범위
이번 개선은 GTM 컨테이너 안의 변수 · 트리거 · 태그 등록 영역에 한정됩니다. 이미 페이지에 심어진 dataLayer 이벤트를 기반으로, GTM 측 설정 작업만 자동화한 케이스입니다. 신규 dataLayer 이벤트 자체를 페이지에 새로 심어야 하는 경우는 여전히 개발 작업이 필요합니다.
.banner .container { max-width: 1152px; margin: 0 auto; padding: 10px 10px; }
안녕하세요, 지로 개발팀입니다.
지로 팀은 신규 기능 배포 때마다 유저의 활동 흐름을 추적하기 위한 장치인 GTM(Googel Tag Manager)을 설정하는데요. 해당 작업은 생각보다 많은 팀의 손을 타왔습니다. 기능 하나에 잦은 요청과 커뮤니케이션이 오갔고, 등록해야 할 GTM 리소스는 기능에 따라 많게는 20개까지 늘어났어요. 신규 기능이 쌓일수록 이 작업도 함께 쌓였고요.
그렇다면 ‘이 과정 자체를 없앨 수 없을까?’ 라는 질문에서 이번 개선이 시작됐어요. 오늘은 개발팀이 1시간 이상 걸리던 GTM 수동 작업을 1분 미만으로 줄인 과정에 대해 소개해 보겠습니다.
📎
*이번 글에서 다루는 ‘자동화’ 범위
이번 개선은 GTM 컨테이너 안의 변수 · 트리거 · 태그 등록 영역에 한정됩니다. 이미 페이지에 심어진 dataLayer 이벤트를 기반으로, GTM 측 설정 작업만 자동화한 케이스입니다. 신규 dataLayer 이벤트 자체를 페이지에 새로 심어야 하는 경우는 여전히 개발 작업이 필요합니다.
…
기존의 GTM 작업 흐름은 이러했습니다.
📎
UX팀이 신규 기능 배포가 결정되면, 데이터팀에 해당 기능의 추적 이벤트 정의를 요청
데이터팀이 추적할 이벤트와 파라미터를 시트에 정의해 제품팀에 전달
개발팀이 (필요 시) dataLayer 코드 작업과 함께 GTM 변수 · 트리거 · 태그를 수동 등록
데이터팀 검수 후 반영 확인
작업 자체는 어렵지 않지만, 기능 1건당 들어가는 리소스 수가 적게는 2개, 많게는 20개까지 늘어났고, 신규 기능이 늘어날 때마다 GTM 작업도 함께 선형으로 증가했습니다. 양 팀 모두에게 보이지 않는 비용이 쌓이고 있었습니다.
따라서 개선 목표는 두 가지로 잡았어요.
📎
정량적 목표: 신규 기능 1건당 발생하던 GTM 수동 작업 시간 최소화
정성적 목표: 개발자가 아니어도 GTM에서 직접 이벤트를 등록할 수 있는 환경을 만들고, 데이터팀이 추적 정의-검수까지 한 번에 마무리할 수 있도록 자율성을 확보. 개발팀은 본 업무에 집중.
핵심은 결국 ‘개발자가 아닌 누구든 GTM에 이벤트를 편하게 등록할 수 있는 환경을 만들자'로 정리되었습니다. GTM 이벤트 추가 과정에서 개발팀의 개입이 반드시 필요한 영역은 막상 크지 않을 것이고, 특히 GTM 설정(변수·트리거·태그) 영역은 데이터팀이 직접 처리 가능한 구조로 전환할 수 있을 것이라고 생각했습니다.
설정 등록이 자동화된다면, 신규 기능이 늘어나도 GTM 설정 작업량이 일정 수준으로 유지될 것이고, 결과적으로 데이터팀은 자기 손에서 정의-등록-검수가 모두 끝나는 흐름을, 개발팀은 GTM 등록 핑퐁에서 벗어나 본 업무에 리소스를 집중할 수 있을 것이라는 가설을 세웠습니다.
자동화 방향을 고민하던 중, GTM 커뮤니티에서 자주 인용되는 Simo Ahava의 글 Google Tag Manager Lookup Table Generator를 먼저 살폈습니다.
이 글은 Google Sheets와 Apps Script, GTM API를 조합해 기존 Lookup Table Variable의 값을 시트에서 관리하고 업데이트하는 방식입니다. 다루는 범위 자체는 "Lookup Table 갱신"으로 우리가 풀려던 "변수·트리거·태그 등록 전반"과는 차이가 있었지만, ‘시트를 기준으로 GTM 설정을 자동화한다’는 발상은 이번 작업의 출발점이 됐습니다.
다만 Apps Script 기반 구현은 디버깅·유지보수에 따로 학습 비용이 들고, 우리 팀이 장기적으로 끌고 가기엔 부담스러운 부분이 있었습니다. 그래서 방향성은 머릿속에 담아두고 실제 적용은 미뤄둔 상태였습니다.
그러던 중 같은 문제를 정면으로 다룬 글을 발견했습니다. GTM 자동화 사이드 프로젝트 — 인프런 기술 블로그였습니다. 읽어보니 저희가 정의한 문제(신규 기능마다 발생하는 GTM 수동 등록)와 해결 방향(시트 기반 자동 등록)이 구조적으로 매우 유사했고, 구현 플로우도 단계별로 상세하게 정리돼 있었습니다. ‘이 구조를 베이스로 가져오면 우리 케이스에도 빠르게 적용할 수 있겠다’는 판단이 들어, 이 글을 출발점으로 작업을 시작했습니다.
전반적인 골격은 레퍼런스의 방식을 따랐습니다. 그 위에 저희 GTM 컨테이너 구조, 사내 이벤트 네이밍 컨벤션, 실제 운영 시나리오에 맞춰 일부 로직과 시트 컬럼 구성을 조정했습니다. 결과적으로 적용된 내용은 크게 네 가지입니다.
📎
실행 환경을 사내 웹 앱으로: Apps Script 기반 구현은 디버깅·유지보수 비용이 신경 쓰여, 모노레포 안에 Next.js 15 사내 웹 앱으로 이식했습니다. 타입 안정성, 코드 리뷰, 배포 파이프라인까지 다른 사내 프로젝트와 같은 방식으로 굴러갑니다.
네이밍·리소스 형태 단일화: 트리거는 gtm.{eventName}, 태그는 GA4 이벤트 태그(gaawe), 변수는 Data Layer Variable로 형태를 한 줄에 모았습니다. 시트만 보고도 GTM에 어떤 이름으로 어떤 형태가 생성될지 예측 가능하게 하는 것이 목표였습니다.
입력 시트 컬럼 재설계: 한 행에 파라미터를 묶어 적는 방식과 파라미터마다 행을 풀어 적는 방식을 모두 허용하도록 컬럼과 파서를 다시 짰습니다. 데이터팀의 기존 작업 습관을 그대로 둘 수 있도록 한 선택입니다.
publish는 자동화에서 의도적으로 제외: 자동화는 워크스페이스에 리소스를 만드는 데까지만, GTM 게시는 사람이 한 번 더 확인하고 누르도록 남겼습니다. (자세한 이유는 아래 반영 단계에서)
입력은 Google Sheets 한 곳에서 모두 들어옵니다. 데이터팀이 별도의 어드민 UI를 쓰지 않고, 평소 이벤트 명세를 관리하던 시트 그대로를 자동화 도구의 입력으로 사용하도록 설계했습니다. 자동화 도구에서는 데이터팀이 작업한 스프레드시트 ID와 탭을 지정해 데이터를 읽어옵니다. 시트의 한 행이 곧 GTM에 등록될 이벤트 1건에 대응 되는데요, 핵심 컬럼은 아래와 같습니다.
컬럼 | 의미 | 예시 |
|---|---|---|
| GTM에 등록될 GA4 이벤트명 (스네이크 케이스) |
|
| 해당 이벤트와 함께 보낼 파라미터 목록 (쉼표·줄바꿈·중괄호 모두 허용) |
|
| 파라미터 단건 정의 (멀티 행으로 풀어 쓸 때 사용) |
|
| 파라미터 값의 타입( |
|
| 이벤트가 발생하는 사용자 동작 분류 |
|
| GTM 안에서 사람이 읽을 표시명 |
|
| 이벤트 설명, 어떤 맥락에서 발화하는지 메모 |
|
여기에 더해, 운영을 위한 보조 컬럼이 함께 들어옵니다.
컬럼 | 역할 |
|---|---|
| 이벤트 분류 (네이밍·그룹핑 기준) |
| 이벤트가 발생하는 페이지 식별 정보 |
| GTM 트리거 타입 매핑 키 (예: |
| QA 통과 / 운영 반영 여부 플래그 |
| 자유 메모 |
한 행에 파라미터를 여러 개 묶어 적어도 되고, 파라미터마다 행을 펼쳐 적어도 됩니다. 자동화 도구가 두 경우 모두를 평탄화해서 동일한 형식으로 처리합니다. 한 줄짜리 단축 표기는 작성 속도를, 여러 줄 펼친 표기는 가독성을 우선할 때 쓰도록 두 가지 방식을 모두 허용했습니다.
데이터팀의 시트와 GTM 컨테이너 사이에는 사내 전용 Next.js 웹 앱을 한 단계 두었습니다. 시트에서 App Script로 직접 GTM API를 호출하는 대신, 웹 앱이 중간에서 시트 데이터를 GTM 리소스로 변환·검증·전송하는 역할을 맡습니다.
도구 자체는 가능한 한 가볍게 굴러가는 구성으로 잡았습니다.
구성 | 스택 | 역할 |
|---|---|---|
프레임워크 | Next.js 15 (App Router) + React 19 + TypeScript strict | 웹 앱 본체. UI + API 라우트를 한 프로젝트에서 처리. |
시트 연동 |
| 스프레드시트 ID·탭 기준으로 이벤트 데이터 조회. |
GTM 연동 | Google Tag Manager API v2 | 계정·컨테이너·워크스페이스·변수·트리거·태그 CRUD. |
서버 상태 | TanStack Query (React Query) | GTM/Sheets 조회·뮤테이션 캐싱, 단계별 미리보기 갱신. |
인증 | Google OAuth 2.0 + 서비스 계정 | 데이터팀은 본인 Google 계정으로 로그인, 서버는 서비스 계정으로 GTM API 호출. |
UI | Tailwind v4 + 사내 디자인 토큰 | 계정·컨테이너·워크스페이스 선택 → 시트 미리보기 → 생성 결과 확인까지의 워크플로 UI. |
API 라우트는 GTM 리소스 단위로 잘게 끊어둬서, 데이터팀이 보는 화면에서는 단일 흐름처럼 보이지만 내부적으로는 다음과 같이 분리되어 호출됩니다.
/api/sheets/tabs, /api/sheets/data : 시트 탭 목록과 행 데이터 조회
/api/gtm/accounts, /api/gtm/containers, /api/gtm/workspaces : GTM 작업 대상 선택
/api/gtm/variables, /api/gtm/triggers, /api/gtm/tags : 시트 데이터를 변수 → 트리거 → 태그 순으로 생성
이렇게 분리해 둔 덕분에, 화면에서 단계마다 ‘생성될 것’을 먼저 미리보기로 보여주고, 데이터팀이 확인 버튼을 누른 시점에만 GTM에 실제로 반영되는 흐름을 만들 수 있었습니다.
시트 한 행은 GTM에서 다음 세 가지 리소스로 펼쳐집니다. 이름과 형태는 시트만 봐도 거의 정확히 예측 가능하도록 규칙을 단일화했습니다.
리소스 | 생성 단위 | 이름 규칙 | 형태 |
|---|---|---|---|
변수 | 시트 전체에서 등장한 | 파라미터명 그대로 (예: | Data Layer Variable ( |
트리거 |
|
| Custom Event 트리거 + 기본 필터 ( |
태그 | 이벤트 1건당 1개 | 시트의 이벤트 식별자에 맞춰 생성 (네이밍 컨벤션 따름) | GA4 이벤트 태그 ( |
{{...}}형태로 이미 GTM에 존재하는 내장 변수를 참조한 경우는 변수 생성 대상에서 제외해, 기존에 정의된 GTM 자산을 재사용합니다. 같은 이벤트명이 시트에 여러 번 등장해도 트리거는 1개로만 만들어지므로, 데이터팀이 행을 자유롭게 늘려 적어도 GTM 안에서 중복 트리거가 쌓이지 않습니다.
GTM 리소스는 한 번 만들고 끝이 아니라, 데이터팀이 시트를 갱신하면서 같은 자동화 도구로 여러 차례 돌리게 됩니다. 그래서 ‘같은 이름의 리소스를 두 번 만들지 않기’가 가장 중요한 검증 포인트였습니다. 도구는 GTM 생성 호출 전에 다음 단계를 순서대로 수행합니다.
📎
요청 입력 검증: accountId, containerId, 생성 대상 배열이 비어 있는지 먼저 확인하고, 비어 있으면 400 응답.
현재 상태 스냅샷 조회: 작업 시작 시점에 해당 워크스페이스의 기존 태그·트리거를 한 번에 가져와 인메모리 Map(name → resource)으로 보관합니다. 이후의 모든 중복 체크는 이 Map에서 처리하므로, 리소스마다 GTM API를 다시 부르지 않습니다.
이름 기반 중복 차단: 생성 직전에 Map에 같은 이름이 있으면 GTM API를 부르지 않고 status: 'duplicate'로 결과에 기록합니다. 실패가 아니라 "이미 만들어져 있음" 으로 다룹니다.
API 응답 fallback: 동시 작업으로 Map 스냅샷 이후에 생성된 리소스라면 GTM 쪽이 "Found entity with duplicate name"으로 응답합니다. 이 메시지를 잡아 같은 duplicate 결과로 흡수해, 부분 실패가 전체 흐름을 멈추지 않게 합니다.
입력 행 사전 정리: 시트에서 가져온 행 중 파라미터가 비어 있거나 평탄화 후 변환 가능한 형태가 아닌 행은 매핑 단계에서 잘라냅니다.
Google API rate limit 가드: 모든 GTM/Sheets 호출은 사내 rate limiter(rateLimitedGoogleApi) 래퍼를 거치도록 강제해, 다량의 리소스를 한 번에 생성할 때도 Google 측 quota를 안전하게 사용합니다.
덕분에 운영 중 자동화로 인한 실패 케이스는 아직 0건입니다. 같은 이름의 리소스가 한 번 더 들어오는 중복은 종종 발생하지만, 위 1~4단계에서 자동으로 duplicate로 처리해 건너뛰기 때문에 데이터팀 작업이 중간에 끊기지 않습니다.
도구가 GTM에 만들어 두는 범위는 명확합니다. 해당 컨테이너의 기본 워크스페이스에 변수·트리거·태그를 생성하는 것까지. 워크스페이스 게시(publish)는 자동화에서 의도적으로 제외했습니다.
이유는 두 가지입니다.
📎
안전장치: GTM의 publish는 운영 환경에 즉시 영향을 주는 행위입니다. 자동화로 publish까지 한 번에 밀고 나가는 것보다, 워크스페이스에 변경을 모아 두고 사람이 한 번 더 확인하는 사이클이 사고를 줄여줍니다.
자연스러운 롤백 지점 확보: 잘못 만들어진 리소스가 있어도 publish 전이라면 워크스페이스를 통째로 폐기/리셋하는 것만으로 깔끔하게 되돌릴 수 있습니다. 운영 환경에는 영향이 없습니다.
인증은 두 단계로 갈라 두었습니다.
📎
데이터팀은 본인 Google 계정으로 사내 웹 앱에 OAuth 로그인합니다. 작업 가능한 GTM 컨테이너·워크스페이스 범위는 해당 사용자가 GTM 측에서 이미 부여받은 권한을 그대로 따릅니다.
실제 GTM API 호출은 사내 서비스 계정 자격으로 서버에서 수행합니다. 클라이언트가 GTM API 토큰을 직접 다루지 않으니, 권한 노출 위험을 줄일 수 있습니다.
롤백은 별도 ‘undo’ 기능을 만들지 않았습니다. 위에서 적은 대로 publish 전 워크스페이스 폐기가 가장 빠르고 안전한 롤백 수단이기 때문입니다. 자동화 도구는 결과 화면에 created / duplicate / failed 를 모두 노출하므로, 어느 리소스가 새로 들어갔는지 사람이 추적할 수 있습니다.
가장 큰 변화는 ‘UX팀 → 데이터팀 → 개발팀 → 데이터팀’의 4단계 핑퐁이 ‘UX팀 → 데이터팀’ 2단계로 짧아진 것입니다. 데이터팀이 필요할 때마다 즉시 게시할 수 있게 되어 업데이트 빈도가 올라갔고, 개발팀이 매번 빼앗기던 시간이 사라졌어요. 아래는 개선 후의 변화를 정리해본 표입니다.
구분 | Before | After |
|---|---|---|
이벤트 정의 → 등록 → 검수 핑퐁 | 4단계 (UX팀 → 데이터팀 → 개발팀 → 데이터팀) | 2단계 (UX팀 → 데이터팀, 데이터팀 안에서 정의·등록·검수 종결) |
기능 1건당 등록되는 GTM 리소스 (태그 + 트리거 + 변수) | 2~20개를 사람이 손으로 등록 (1시간 이상 소요) | 2~20개를 시트 입력으로 일괄 생성 (1분 미만) |
업데이트 빈도 | 개발팀 일정에 묶여 산발적으로 반영 | 데이터팀이 필요한 시점에 바로 게시 가능 |
데이터팀 셀프 처리율 (GTM 설정 영역) | 0% (개발팀 작업 필수) | 100% (개발팀 개입 없이 마무리) |
자동 처리 안정성 | (수동 작업이라 휴먼 에러 위험 상존) | 실패 0건. 중복은 자동 |
개발팀 GTM 설정 작업량 | 기능 수에 비례 증가 | 0에 수렴 (본 업무에 시간 회수) |
잘 사용하던 중, 내부 웹 앱에서 생성한 GA를 PROD 외 스테이지 (e.g., DEV)에 연결하는 과정에서 동일한 GA Key로 스테이지별로 컨테이너를 각각 생성해야 하는 번거로움이 있었습니다.
기존에는 같은 GA Key를 사용하는 스테이지별 컨테이너에 각각 접속해서 동일한 이벤트 리소스를 따로 등록해야 했습니다. 자동화 도구로 리소스 생성 자체는 빨라졌지만, 여러 컨테이너를 오가며 같은 작업을 반복해야 하는 불편은 남아 있었습니다.
그래서 이 과정을 개선하여 스테이지별 컨테이너를 한 번에 생성할 수 있는 기능도 추가했습니다. GTM 컨테이너 선택 화면에 Dev 동기화 토글을 추가했습니다. 토글을 켜면, Prod 컨테이너와 연결된 Dev 컨테이너를 자동으로 탐색해 동일한 리소스 생성 작업을 동시에 실행합니다.
자동화 도구가 안정적으로 운영되면서, 도구가 이미 알고 있는 정보(시트에 정의된 이벤트명과 파라미터 구조)를 프론트엔드 개발에도 직접 활용할 수 있겠다는 생각이 들었습니다.
기존에는 개발자가 dataLayer에 이벤트를 심을 때 어떤 이벤트명과 파라미터가 있는지 시트나 GTM 화면을 직접 들여다봐야 했고, 오탈자나 파라미터 누락은 런타임까지 발견하기 어려웠습니다.
이를 해결하기 위해 같은 스프레드시트를 기반으로 TypeScript 타입을 자동 생성하는 CLI 도구 build-gtm-types를 함께 만들었습니다. 시트의 각 탭에 정의된 이벤트명·파라미터·설명을 읽어 다음과 같은 판별 유니온(discriminated union) 타입을 생성합니다.
// ! build-gtm-types 패키지에 의해 자동 생성됐습니다. 수동으로 수정하지 마세요.
type AccountEvents =
/** 공통 벨트 클릭 */
| {
event: 'belt_click';
params: {
belt_id: string;
belt_name: string;
}
}
/** 다른 이벤트 */
| {
event: 'foo';
params?: {
bar: string;
baz: string;
};
};스프레드시트 탭마다 파일 하나가 생성되고, 전체를 합친 GTMEvents 타입을 진입점으로 내보냅니다. 개발자는 dataLayer에 이벤트를 심을 때 이벤트명과 파라미터가 자동 완성되고, 오탈자나 파라미터 누락을 컴파일 타임에 잡을 수 있습니다.
sendGTMEvent<GTMEvents>({
event: 'belt_click', // 자동완성 + 오탈자는 컴파일 에러
params: { belt_id: '123', belt_name: '기본 벨트' },
});데이터팀이 시트에 이벤트를 정의하면 도구가 GTM에 자동 등록하고, 개발자는 같은 시트를 기반으로 생성된 타입을 통해 dataLayer를 타입 안전하게 사용합니다. 이벤트 정의의 단일 진실 공급원(source of truth)이 스프레드시트 하나로 유지됩니다.
이번 개선은 각 팀이 매일 조금씩 잃고 있던 시간을 되찾는 일이었고, 그래서 더 만족도가 높은 프로젝트였습니다. 하지만 자동화를 했다고 해서 끝이 아닌, 데이터팀이 실제로 운영하면서 나오는 피드백을 바탕으로 자주 발생하는 케이스에 대한 가이드 보강과 휴먼 에러를 사전에 막는 검증 장치 추가를 이어서 검토하고 있습니다.
지로의 프론트엔드팀은 제품 기능을 만드는 것에서 멈추지 않습니다. 반복되는 운영 병목을 발견하면, 그것을 시스템으로 바꿔 동료 팀의 일하는 방식 자체를 가볍게 만드는 일까지 우리의 일이라고 생각합니다. 매번 들어오는 요청을 처리하기보다 그 요청이 더 이상 필요 없는 구조를 설계하는 데에 관심이 있는 분이라면, 지로의 문을 두드려 주세요!