날씨앱을 개발하고 서비스하면서
사용자 경험을 더 정교하게 만들고 싶다는 욕심이 생겼다 🌤️
그러기 위해서, 의사결정 근거로써 사용자 로그 데이터를 수집하고 싶었다.
복잡한 서버 연동 없이 앱 단에서 수집할 수 있는 구조를 만들고자 했고, 그 결과 Google Sheets 를 활용하여 경량-무서버 로그 수집 시스템을 구성하게 되었다.
이 글에서는 시스템의 환경 구축과 구현 과정을 중심으로 기록해 보았다.
Google Sheets는 별도의 서버나 DB 구축 없이도 데이터를 실시간으로 저장하고 확인할 수 있어 초기 구성 부담이 적다. Google Apps Script를 통해 HTTP 요청을 처리할 수 있어 간단한 로깅 서버 역할도 가능하다.
React Native 환경에서도 fetch 기반으로 쉽게 연동할 수 있고, 개발·운영 비용 없이 빠르게 테스트 가능한 점이 실용적이었다 무거운 인프라 없이 가볍게 로그 수집을 시작하기에 합리적이라 판단하였다.
결과를 먼저 소개한 뒤,
이를 가능하게 만든 환경 구축 과정을 차례대로 정리해 보았다.
사용자 로그 데이터 수집 자동화 환경을 구축하면
앱을 사용함에 따라 구글 스프레드시트에 그 로그 데이터가 적재된다.
시트별로 action에 따라 필요한 데이터를 수집할 수 있는데,
해당 이미지에서는 앱 실행(app_open) 시 수집 가능한 가장 기본적인 데이터들이 쌓인 것을 볼 수 있다.
로그 데이터 수집한 아키텍처는 아래처럼 설계하였다.
React Native App ──> fetch() ──> Google Sheets API
│ ▲
├─ AsyncStorage: Access / Refresh ┘
└─ logEvent() util
2025.05.20 기준으로 작성되었다.
장기적인 로그 수집을 위해서 일반 계정 활성화 를 통해 결제 계정 업그레이드하여 사용하고 있다.
공식 문서에 따르면, Google Sheets API 자체는 추가 요금이 없다. 과금 SKU 자체가 없어서 호출이 많아도 비용이 0 원으로 찍힌다. 다만 초당·분당 쿼터를 넘기면 429(Too Many Requests)를 받게 되기는 한다. 과금 계정은 ‘신용카드 보증’일 뿐, Sheets API에는 실제 요금이 발생하지 않는 것이다.
다만, 무료 평가판이 끝나면 프로젝트 결제가 막혀 API도 동작하지 않기 때문에 일반 계정 활성화를 통해 업그레이드하여 사용해야 한다.
예측 불가한 트래픽 대비하여 업그레이드 후에는 Budget 알림과 쿼터 모니터링으로 예기치 않은 과금을 방지하는 것이 필요하겠다.
Google Cloud Console에 접속하여 구글 계정으로 로그인
상단에서 프로젝트 선택 > 새 프로젝트 클릭 
Google Sheets API 활성화
API 및 서비스 > 라이브러리 로 이동사용 버튼 클릭해서 활성화


사용자가 인증된 상태로 API에 접근하도록 Access Token을 발급받기 위한 과정이다.
왼쪽 메뉴에서 API 및 서비스 > 사용자 인증 정보 로 이동

OAuth 동의 화면 구성 -> 이때 대상: 외부(External)


상단에서 + 사용자 인증 정보 만들기 → OAuth 클라이언트 ID 선택

OAuth 클라이언트 ID 만들기

[참고]
💡 애플리케이션 유형을 웹 애플리케이션으로 지정하지 않으면, redirect URI를 입력할 수 없다.
💡 승인된 리디렉션 URI (Authorized redirect URIs) 항목에 해당 주소를 추가하지 않으면, 이후 구글 계정 인증 단계에서 엑세스 요청이 차단되는 400: redirect_uri_mismatch 에러가 발생하게 된다.
여기까지 진행하면 Client ID와 Client Secret이 발급된다.
이후, 아래의 화면에서 확인되는 클라이언트 ID와 클라이언트 보안 비밀번호를 사용하게 된다.

3-2 까지 수행 후, 곧바로 3-4로 넘어갔을 때 아래와 같은 에러를 만났다.

에러의 원인은 OAuth 동의 화면을 외부 사용자에게 공개하지 않았기 때문인 것으로 확인되었다.
현재 Google Cloud Console에서 만든 OAuth 클라이언트의 동의 화면은 "테스트 중" 상태이며, Google 계정 중 등록된 테스트 사용자만 접근할 수 있도록 제한되어 있는 것이다.
[ 해결 방법 ]
API 및 서비스 > OAuth 동의 화면 > 대상 으로 이동
OAuth 클라이언트가 만들어졌으면 이제 Access Token을 발급받아야 한다.
가장 간단한 방법은 OAuth 2.0 Playground를 이용하는 것이다.
오른쪽 상단 톱니바퀴(⚙️)
ⅰ. "Use your own OAuth credentials" 체크
ⅱ. 발급받은 Client ID, Client Secret 입력

[Setp 1] API 스코프 선택
https://www.googleapis.com/auth/spreadsheets 스코프 선택 (직접 입력도 가능)

활성화된 Authorize APIs 클릭 → Google 계정 로그인
[Step 2] Authorization Code 발급
Exchange authorization code for tokens 클릭
➱ Refresh token 및 Access token 발급 완료
Auto-refresh the token before it expires.
→ OAuth Playground는 테스트 용이기 때문에 체크할 필요는 없음

이렇게 발급받은 토큰을 사용하여 Google Sheets에 접근할 수 있게 된다.
발급된 Access Token은 1시간(3600초) 후 만료된다. 따라서 Playground에서 함께 발급받은 Refresh Token을 이용해 자동 갱신 로직으로 구현하였다.
API 활성화와 토큰 발급 과정이 핵심이지만, React Native 기반 기술적인 구현 과정도 간략하게 정리해 보았다.
| 항목 | 설명 | 예시 |
|---|---|---|
| 스프레드시트 ID | Google Sheets 문서의 ID | 1AbCDeFgHijKlmNoPqRstUvWxYz1234567890 |
| 시트 이름 | 보통은 Sheet1 | Sheet1 |
| Access Token | 방금 발급받은 토큰 | ya29.a0AZYkNZJl... |
로그 데이터 수집을 위해 구글 스프레드시트를 생성해야 한다.
스프레드시트 ID는 URL에서 확인 가능
➱ https://docs.google.com/spreadsheets/d/[여기가 ID]/edit
API가 접근하려면 스프레드시트에 사용자 계정(서비스 계정이거나 OAuth 로그인한 계정)을 편집자로 초대해야 한다.
Sheet 이름은 대소문자가 구분된다.
→ Sheet1, sheet1 은 다름
Playground에서 받아온 토큰들을 바로 사용할 수 없다.
GitHub Push Protection, 보안의 마지막 경고에서 언급된 사례가 바로 이 상황이었다.
민감한 CLIENT_ID, CLIENT_SECRET 값은 .env 에 두고, 앱 빌드 시 Babel 플러그인이나 EAS config로 주입한다.
현재 단계에서는 Refresh Token을 기기에 보관하긴 하지만, 실서비스에서는 Cloud Functions나 프록시 서버로 옮기면 더 안전하다.
// .env
GOOGLE_ACCESS_TOKEN=ya29...
GOOGLE_REFRESH_TOKEN=1//..
GOOGLE_CLIENT_ID=...apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=...
앱이 처음 실행될 때, Google Sheets API를 사용하기 위한 토큰이 없다면 .env에서 불러온 토큰을 AsyncStorage에 저장한다. 이미 저장되어 있다면 초기화를 생략하여 불필요한 덮어쓰기를 방지한다.
// App.js
// Google Sheets API 사용을 위한 Access Token 및 Refresh Token 초기 저장
useEffect(() => {
const ensureSheetsTokens = async () => {
const [[, access], [, refresh]] = await AsyncStorage.multiGet([
'accessTokenForSheets',
'googleRefreshToken',
]);
if (!access || !refresh) {
// 토큰 저장 (.env 파일에 별도 관리)
await AsyncStorage.multiSet([
['accessTokenForSheets', process.env.GOOGLE_ACCESS_TOKEN],
['googleRefreshToken', process.env.GOOGLE_REFRESH_TOKEN],
]);
...
};
ensureSheetsTokens();
}, []);
React Native 앱에서 Google Sheets를 원격 로그 저장소로 쓰기 위해 필요한 전 과정을 자동화하기 위해 다음과 같이 핵심 로직을 구상하였다.
목표는 "Access Token은 자동으로 갱신되어 로그 유실이 최소화되고, 시트에는 한국 시간 기준의 깔끔한 로그 테이블을 쌓는 것" 이다.
KST 타임스탬프 생성
"YYYY-MM-DD hh:mm:ss" 형식 문자열 반환OAuth 2.0 토큰 관리
Google Sheets 한 줄 추가(append)
실제 로그 이벤트 함수
추우 출석 전용 이벤트, 게시글 작성 전용 이벤트 등 유의미한 인사이트를 도출해 낼 수 있는 데이터 수집을 고려하고 있다.
// api > googleSheetLogger.js
import {Platform} from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import {GOOGLE_CLIENT_ID as CLIENT_ID, GOOGLE_CLIENT_SECRET as CLIENT_SECRET} from '@env';
const SPREADSHEET_ID = '...';
const SHEET_NAME = '...';
// 1) KST 타임스탬프
const getKSTTimestamp = () =>
new Date(Date.now() + 9 * 60 * 60 * 1000)
.toISOString()
.replace('T', ' ')
.substring(0, 19);
// 2) RefreshToken으로 AccessToken 재발급
const refreshAccessToken = async () => {
const refreshToken = await AsyncStorage.getItem('googleRefreshToken');
if (!refreshToken) throw new Error('...');
const res = await fetch('https://oauth2.googleapis.com/token', {
method : 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body : new URLSearchParams({
client_id : CLIENT_ID,
client_secret: CLIENT_SECRET,
refresh_token: refreshToken,
grant_type : 'refresh_token',
}).toString(),
});
if (!res.ok) throw new Error('...');
const {access_token} = await res.json();
await AsyncStorage.setItem('accessTokenForSheets', access_token);
await logRefreshTokenUsage();
return access_token;
};
// 3) Google Sheets append 유틸
const appendToGoogleSheet = async (values, sheetName = SHEET_NAME) => {
const request = async token =>
fetch(
`https://sheets.googleapis.com/v4/spreadsheets/${SPREADSHEET_ID}/values/${sheetName}!A1:append?valueInputOption=USER_ENTERED`,
{
method : 'POST',
headers: {Authorization: `Bearer ${token}`, 'Content-Type': 'application/json'},
body : JSON.stringify({values: [values]}),
},
);
let token = await AsyncStorage.getItem('accessTokenForSheets');
let res = await request(token);
// 만료(401) → 자동 재발급 후 재시도
if (res.status === 401) {
token = await refreshAccessToken();
res = await request(token);
}
if (!res.ok) throw new Error('...');
};
// 4) RefreshToken 사용 기록
const logRefreshTokenUsage = async () =>
appendToGoogleSheet(
[getKSTTimestamp(), 'system', 'refresh_used', '-', '-', '-', '-', '-', Platform.OS],
'Sheet2',
);
// 5) 실제 사용자 액션 로그
export const logUserAction = async ({...}, actionName) =>
appendToGoogleSheet(
[
...
],
);
데이터 수집 구조를 설계할 때 가장 우선되어야 할 요소는 법적·윤리적 기준을 준수하는 것이다.
사용자 식별을 위한 민감 정보 수집을 배제하고, UUID(Universally Unique Identifier) 기반으로 로그를 관리함으로써 최소 수집 원칙을 따르고 있다.
또한 첫 실행 시 개인정보 처리방침과 로그 수집 목적을 사용자에게 명확히 고지하고, 동의를 받는 절차를 포함하였다. 수집된 데이터는 내부 분석 용도로만 활용되며, 광고 등 다른 목적으로의 사용은 금지된다.
보관 기간 역시 1년으로 한정하고 있으며 이후 자동 파기될 수 있도록 설계하였다. 국외 서버(Google Sheets)를 사용하는 구조 특성상 개인정보 국외 이전에 대한 안내와 동의 항목도 정책 내에 포함해야 한다.
향후에도 개인정보 보호법(PIPA) 및 관련 가이드라인에 따라 체계를 지속적으로 점검하고 개선할 예정이다.
현재는 테스트 환경에서 Google Sheets에 로그 데이터를 수집하고 있지만, 운영 단계에서는 보다 안정적이고 분석 친화적인 구조로 확장할 계획이다. 앱에서 민감한 인증 정보를 직접 다루지 않도록 운영용 시트는 테스트 환경과 분리하여 데이터 정확성과 보안을 강화할 예정이다.
수집된 로그는 Google Sheets에서 일정 주기로 CSV 형태로 추출하거나 BigQuery로 연동하여 대용량 데이터도 유연하게 다룰 수 있는 기반을 마련할 계획이다. 이후 시계열 흐름, 기능별 사용 패턴, 앱 버전별 변화 등을 Tableau 기반 대시보드로 시각화하여 사용자 행동을 종합적으로 해석할 수 있도록 구성할 예정이다.
또한, 쌓인 데이터를 분석 가능한 구조로 정제하여 다양한 방식의 탐색적 분석과 지표 계산이 가능하도록 관리할 예정이다. 이를 통해 앱 운영의 방향성을 도출하고, 데이터 기반의 의사결정을 체계화할 수 있는 환경을 갖춰나갈 계획이다.