Skip to Content
로깅

로깅

얼마집 프론트엔드는 Datadog(이벤트/RUM)과 Sentry(에러 트래킹) 두 가지 모니터링 도구를 사용합니다.

핵심 원칙

앱에서는 @datadog/browser-rum, @datadog/browser-logs, @sentry/nextjs 패키지를 직접 호출하지 말고, 반드시 logger 패키지를 통해 로깅하세요.

logger 패키지는 두 도구의 초기화·사용자 컨텍스트 설정·호출 방식을 통일된 인터페이스로 추상화합니다. 직접 호출 시 초기화 순서 문제, 사용자 컨텍스트 누락, 서버/클라이언트 환경 혼용 등의 문제가 발생할 수 있습니다.

초기화

Sentry — initSentry

  • 클라이언트: 앱 루트의 instrumentation-client.ts에서 호출합니다. (Next.js 15.3+/Sentry v9+ 권장 — 구 sentry.client.config.ts 대체)
  • 서버/엣지: instrumentation.tsregister()에서 런타임 가드 + 동적 import 로 호출합니다. (구 sentry.server.config.ts/sentry.edge.config.ts 대체 — server/edge 설정이 동일하므로 한 곳에서 처리)
// instrumentation-client.ts (클라이언트) import {initSentry} from '@howmuchhome-web/logger/sentry'; import * as Sentry from '@sentry/nextjs'; initSentry(); // App Router 클라이언트 내비게이션 instrumentation export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
// instrumentation.ts (서버/엣지) export async function register() { if ( process.env.ENV === 'production' && (process.env.NEXT_RUNTIME === 'nodejs' || process.env.NEXT_RUNTIME === 'edge') ) { const {initSentry} = await import('@howmuchhome-web/logger/sentry'); initSentry(); } }

initSentryNEXT_PUBLIC_ENV === 'production'일 때만 실제로 초기화합니다.

initSentry는 브라우저 전용 패키지를 포함하지 않도록 서브패스(/sentry)로 분리되어 있습니다. 메인 패키지(@howmuchhome-web/logger)가 아닌 서브패스에서 import해야 합니다.

Datadog RUM + 사용자 컨텍스트 — LoggerProvider

앱 루트에 LoggerProvider를 마운트합니다. 내부적으로 다음을 모두 처리합니다.

  • Datadog RUM 초기화 (datadogRum.init)
  • Datadog RUM 사용자 컨텍스트 설정 (datadogRum.setUser)
  • Sentry 사용자 컨텍스트 설정 (Sentry.setUser)
  • OpenTelemetry span 속성 기록 (서버)
// app/layout.tsx (Server Component) import {LoggerProvider} from '@howmuchhome-web/logger'; export default async function RootLayout({children}) { const user = await getCurrentUser(); return ( <html> <body> <LoggerProvider user={user}>{children}</LoggerProvider> </body> </html> ); }

usernull이면 anonymous-{timestamp}-{random} 형식의 익명 ID가 자동으로 생성됩니다.

서비스 이름을 변경해야 하는 경우 service prop을 전달합니다 (기본값: 'howmuchhome-web').

<LoggerProvider user={user} service="howmuchhome-web-admin"> {children} </LoggerProvider>

로그 기록 — Logger

일반 로그 (Datadog)

클라이언트 사이드에서 커스텀 로그를 Datadog Browser Logs로 전송합니다.

import Logger from '@howmuchhome-web/logger'; // 기본 로그 (type 기본값: 'info') Logger.log('사용자가 부동산 상세 페이지를 조회했습니다.'); // 로그 레벨 및 컨텍스트 지정 Logger.log('검색 결과가 없습니다.', { type: 'warn', context: {keyword: '강남구', resultCount: 0}, });
type설명
'ok'정상 완료
'info'일반 정보 (기본값)
'warn'경고
'error'에러
'debug'디버그

사용자 행동 추적 (Datadog RUM)

Datadog RUM에 커스텀 액션을 기록합니다.

// 버튼 클릭, 폼 제출 등 사용자 인터랙션 추적 Logger.addAction('부동산_찜하기', {propertyId: '123', source: 'detail_page'}); Logger.addAction('검색_실행', {keyword: '강남구', filters: {minPrice: 5000}});

뷰 전환 추적 (Datadog RUM)

SPA에서 URL 변경과 무관하게 논리적 뷰 전환을 추적할 때 사용합니다.

Logger.startView('부동산_상세'); Logger.startView('청약_신청_퍼널');

에러 캡처 (Sentry)

에러를 Sentry로 전송합니다. 서버/클라이언트 양쪽에서 모두 사용할 수 있습니다.

import Logger from '@howmuchhome-web/logger'; import {ErrorLevel} from '@howmuchhome-web/error'; try { await submitApplication(data); } catch (error) { Logger.captureError(error as Error, { transactionName: '청약_신청', level: ErrorLevel.ERROR, tags: {feature: 'application', step: 'submit'}, extra: {applicationId: data.id}, }); }

level 옵션 (ErrorLevel)

설명
ErrorLevel.FATAL치명적 오류
ErrorLevel.ERROR일반 오류 (기본값)
ErrorLevel.WARNING경고 수준
ErrorLevel.INFO정보성 이벤트
ErrorLevel.IGNORESentry 전송 생략

ErrorLevel.IGNORE를 지정하면 Sentry에 전송되지 않습니다. 예상 가능한 에러(사용자 취소 등)에 활용하세요.

fingerprint 옵션

동일한 에러를 하나의 Sentry 이슈로 그룹핑할 때 사용합니다.

Logger.captureError(error, { fingerprint: ['payment-timeout'], });

API 에러 처리와의 관계

API 요청 에러는 @howmuchhome-web/api 패키지의 fetcher가 자동으로 Sentry에 전송합니다. @SilentErrorCodeList 데코레이터로 특정 에러 코드를 전송에서 제외할 수 있습니다. 별도로 Logger.captureError()를 호출할 필요가 없습니다.

직접 Logger.captureError()를 사용하는 경우:

  • API 이외의 비즈니스 로직 에러
  • 외부 SDK 에러
  • 클라이언트 사이드에서만 발생하는 예외

API 에러 처리 — 예상 실패는 Datadog 집계 / 미처리는 Sentry error

원칙: 복구·처리된 예상 실패는 Datadog(집계), 처리가 누락됐거나 치명적인 에러는 Sentry error.

처리된(예상된) API 실패는 Datadog RUM custom action(api_error) 으로 집계해 어떤 실패가 얼마나 자주 발생하는지(errorCode 기준)를 분석하고, 처리 누락은 Sentry error로 잡습니다. web·dashboard·native 세 표면 모두 동일하게 적용됩니다. (IGNORE 레벨 에러 — 401/사용자 취소 등 — 은 항상 자동 스킵됩니다.)

Sentry로는 미처리/치명적 에러만 남겨 이슈 노이즈·쿼터를 줄입니다.

분류 필드 (api_*)

ApiServerError는 아래 필드를 자동으로 답니다. 이 필드는 Datadog RUM action attribute(예상 실패 집계)와 Sentry event.tags(미처리 error, sentryBeforeSend가 병합) 양쪽에 동일하게 실립니다.

필드
api_statusHTTP 상태코드404, 422
api_methodHTTP 메서드GET, POST
api_path정규화된 경로(id → {id})/vote/{id}/ballot
api_error_code서버 errorCodeVOTE_ALREADY_SUBMITTED
  • Datadog: RUM Explorer에서 @action.name:api_error @api_error_code:* facet으로 집계하거나 RUM-based metric으로 대시보드/monitor를 만든다.
  • Sentry(미처리 error): api_error_code:VOTE_ALREADY_SUBMITTED 태그로 필터. 이슈 그룹핑은 fingerprint = [path, method, status, errorCode]로 분리됨.

GET (query)

ErrorBoundary가 자동으로 처리합니다 — 폴백을 띄워 서브트리를 복구하면 Sentry warning, 최상위 크래시 바운더리(errorLevel={ErrorLevel.ERROR})는 error. 별도 조치가 필요 없습니다.

뮤테이션 (useAPIMutation)

표준: 에러 UX(toast/alert)는 API 정의의 @MutationOptions onError 에 두고, 호출부는 await mutateAsync() + try/catch 로 성공 후처리를 게이팅한다.

onError(데코레이터/호출부) 또는 errorLevel 이 있으면 = “처리됨” 신호useAPIMutation 이 내부에서 reportHandledError 를 호출한다.

  • errorLevel 미지정: 예상 API 에러(api_error_code) → Datadog RUM action(api_error) 집계(Sentry X). 이때 그 에러는 silent 로 마킹되어, await mutateAsync 를 잡지 않아도 전역 unhandledrejection 의 Sentry 이중이 뜨지 않는다(→ 호출부 빈 catch 가 안전).
  • errorLevel 지정: 그 레벨로 Sentry 에스컬레이션(“이건 실제 에러다”). 이 경로는 마킹하지 않아 Sentry 로 간다.
  • 비-API(예상 밖) 처리 에러 → Sentry warning(가시성 유지).
  • 어느 신호도 없으면 개입하지 않는다 → 처리 누락 mutateAsync uncaught 는 전역 unhandledrejection 이 Sentry error 잡는다.

에러 UX 를 api 정의(@MutationOptions onError) 에 두면 그 엔드포인트의 전 호출부 공통 기본 동작이 되고(중복 제거), 동시에 “처리됨” 신호가 되어 Datadog 집계에 편입된다. 알림은 @howmuchhome-web/imperative-uitoast/alert 로 띄운다(ui 무의존, 컨텍스트 밖에서도 호출 가능). 자세한 건 ui 문서의 “명령형 toast / alert”.

// (1) api 정의: 기본 에러 UX + "처리됨" 신호(→ Datadog 집계) import {toast} from '@howmuchhome-web/imperative-ui'; @MutationOptions(client => ({ onSuccess: () => { client.invalidateQueries({queryKey: getQueryKey(QuizAPI.getQuizList)}); }, onError: () => toast.error('퀴즈 제출에 실패했어요'), })) @POST(props => `v1/quiz/${props.quizId}/solve`) static async solveQuiz(props: {quizId: string; answer: string}) { /* ... */ }
// (2) 호출부: await + try/catch 로 성공 후처리 게이팅. 에러는 데코레이터 onError 가 처리하므로 catch 는 삼킨다. const quizSolveMutation = useAPIMutation(QuizAPI.solveQuiz); const handleAnswer = async (quizId: string, answer: string) => { try { await quizSolveMutation.mutateAsync({quizId, answer}); } catch { return; // 에러는 데코레이터 onError(toast + Datadog 집계)가 처리 → 성공 후처리 중단 } openResultSheet(); // 성공 후처리 };

호출부에서 문구를 다르게 하거나 Sentry 로 올리려면 그 useAPIMutationonError/errorLevel 로 override 한다.

errorCode 별 구체 처리 — 호출부 config onError override

화면이 errorCode 마다 다른 안내(특정 alert/문구)나 컴포넌트 동작(상태 리셋·언마운트 가드·네비게이션)을 해야 하면, 그 분기를 호출부 useAPIMutation(API.x, {onError}) 에 둔다. config onError데코레이터의 generic 기본 toast 를 override 하므로(이중 없음), errorCode별 구체 안내만 뜬다. 호출부 클로저라 alert 컨트롤러·setState·ref·router 등 컴포넌트 스코프에 모두 접근할 수 있다.

const alreadyAlert = useAlert(); // errorCode 별 구체 처리는 호출부 onError 로(데코레이터 기본 toast override). const submitMutation = useAPIMutation(VoteAPI.voteSubmission, { onError: error => { if ( ApiServerError.isApiServerError(error) && error.isErrorOfType('VOTE_ALREADY_SUBMITTED') ) { alreadyAlert.open(); // 화면별 alert 컨트롤러 } else { toast.error('제출에 실패했어요'); // 그 외 일반 안내 } setIsSubmitting(false); // 에러 시 컴포넌트 상태 정리 }, }); const handleSubmit = async () => { try { await submitMutation.mutateAsync(vars); } catch { return; // 에러는 onError 가 처리 → 성공 후처리만 중단 } goNext(); // 성공 후처리 };

왜 데코레이터가 아니라 호출부인가 — 데코레이터는 공유 packages/api 에 있어 (1) 컴포넌트 컨텍스트(alert 컨트롤러·state·언마운트 가드)에 접근할 수 없고, (2) 화면별 UX 문구를 데이터 레이어로 끌어들이게 된다. 그래서 역할을 나눈다: generic 기본 toast 는 데코레이터(엔드포인트 안전망), errorCode별 구체 UX 는 호출부 onError. 호출부에 onError 가 있으면 데코레이터 기본은 자동으로 덮이므로 이중이 나지 않는다.

정책:

  • 비-GET API 호출은 컴포넌트/훅에서 useAPIMutation 으로 한다.
  • 에러 UX 는 @MutationOptions onError 에(표준, 처리됨 신호). 호출부는 통일성을 위해 mutateAsync + try/catch(성공 후처리가 없으면 빈 catch 라도 자연스럽다).
  • errorCode별 구체 안내·컴포넌트 동작(특정 alert·상태 리셋·언마운트 가드 등)이 필요하면 호출부 config onError 로 처리한다(데코레이터 generic 기본 toast 를 자동 override → 이중 없음). 위 errorCode 별 구체 처리 참조.
  • onError/errorLevel 없이 try/catch 만으로 처리하는 곳은 런타임에 처리 여부가 안 보여 집계되지 않는다(동작엔 문제 없음). onError 로 옮기면 집계에 편입 — 점진 이관 대상.
  • (참고) 미처리 뮤테이션을 호출 방식과 무관하게 Sentry error 로 직접 보고하는 안전망 자리가 useAPIMutation 에 있으나, 기존 try/catch-only 사이트의 일괄 스파이크를 막기 위해 현재 비활성(주석) 이다. web try/catch → onError 이관(또는 lint) 후 활성화한다.

서버액션 (server action)

서버액션은 서버에서 ApiServerError를 잡아 serializeForServerAction()으로 직렬화해 던집니다. 클라이언트 catch에서 reportServerActionError 로 복원 + 동일 기록(예상 API 에러 → Datadog, 예상 밖 → Sentry error)을 합니다.

import {reportServerActionError} from '@howmuchhome-web/api'; try { await someServerAction(args); } catch (e) { const err = reportServerActionError(e); // 예상 API 에러 → Datadog, 예상 밖 → Sentry error if (err?.statusCode === 401) toast.show({message: '비밀번호가 맞지 않아요'}); }

Native (Expo)

native 앱(apps/native-app)도 위와 동일한 @howmuchhome-web/logger(Logger) API 를 씁니다. 애플리케이션 코드는 web·native 구분 없이 Logger.log / addAction / startView / captureError 만 호출하며, Datadog·Sentry SDK 를 직접 부르지 않는 원칙도 그대로입니다.

차이는 그 아래 구현뿐입니다. 공유 packages/logger/index.ts 는 web 전제로 @datadog/browser-logs·@datadog/browser-rum·@sentry/nextjs 를 직접 import 하는데, 이 셋은 RN 에서 못 씁니다. 공유 패키지를 수정하지 않고, apps/native-app/metro.config.js 의 커스텀 resolver 가 native 번들에서만 Datadog 브라우저 SDK 를 mobile 어댑터 shim(@datadog/mobile-react-native 기반)으로, @sentry/nextjs@sentry/react-native 로 갈아끼웁니다. 초기화·RUM/에러 필터·소스맵 등 native 세부는 별도로 다룹니다.

자세한 내용은 native 로깅·관측을 참고하세요.

사례 — 간편인증(eziok) 계측

간편인증은 외부 인증앱(카카오·네이버·토스…)을 다녀와야 끝나는 플로우라 실패가 조용합니다. 앱 프로세스가 OS 에 종료되거나 사용자가 인증앱에서 이탈하면 우리 서버에는 아무 요청도 남지 않습니다. 실측(2026-08-21 프로덕션)으로 인증 시작 33건 대비 결과 도달 18건이었고, 서버측 검증 실패는 0건이었습니다 — 즉 “어디서 끊겼는지”는 서버 로그만으로 알 수 없습니다.

그래서 이 플로우는 세 계층에 나눠 계측합니다. 계층을 잇는 상관키는 경로마다 다릅니다 — 표준창 경로는 clientTxId(BFF 발급, URL state 로 왕복)를, API 경로(siv_api_* 이벤트)는 transactionId(서버 거래 ID)를 상관키로 씁니다.

계층어디에이벤트
web (RUM/Logs)apps/app/src/features/eziok/logging.tssiv_view_opened · siv_auth_start · siv_auth_start_failed · siv_auth_failed · siv_result_received · siv_returned_without_result · siv_save_log_ok · siv_save_log_failed · siv_low_power_guide_shown · siv_api_start_ok · siv_api_start_failed · siv_api_fallback_to_std · siv_api_app_link_opened · siv_api_terminal · siv_api_resumed_waiting
BFF 라우트 (Vercel 런타임 로그)apps/app/lib/eziok/log.tsrequest_issued · request_failed · key_not_configured · pending_recorded · pending_record_failed · prefill_applied · prefill_skipped · result_ok · result_tx_mismatch · result_issue_date_expired · result_verify_failed · result_no_key_token · result_no_hub_token · result_exception
native 셸 (RUM/Logs)apps/native-app/src/app/native/assignment-simple-identity-verification.tsx, src/components/SimpleIdentityVerificationResumer.tsxsiv_native_screen_opened · siv_native_backgrounded · siv_native_external_app_open · siv_native_returned_from_external_app · siv_native_bounced_to_start · siv_native_webview_process_gone · siv_native_hardware_back · siv_resumed_after_process_kill

BFF 로그에는 개인정보를 넣지 않습니다. 인증 명의(이름·생년월일·휴대폰)·encCi·hubToken·keyToken 은 실명과 자격증명이고, Vercel 런타임 로그는 접근 통제가 이 값들에 맞춰져 있지 않습니다. 표준창 자동 채우기(prefill_applied)도 적용 여부와 필드 수만 남기고 평문은 라우트 핸들러 스코프를 벗어나지 않습니다. clientTxId 만 예외로 남기는데, 10분 TTL 단회용 거래 식별자이고 이미 URL state 로 왕복하므로 계층을 잇는 상관키로 쓸 수 있습니다.

표준창 되돌림(C 버킷) 기전 판별 (2026-09-15, HMH-9302)

인증앱에서 살아 돌아왔는데 결과가 없는 케이스(C 버킷)는 web 계측만으로는 기전을 알 수 없습니다. siv_returned_without_result 는 “결과 없이 다시 열렸다”는 사실만 알고, 직전 페이지가 cross-origin 표준창이라 어디서 어떻게 돌아왔는지 모릅니다. 그래서 native 셸이 WebView 를 지켜봅니다. 벤더 스펙상 표준창 결과는 브라우저를 거쳐서만 도달하고 허브가 결과를 5분만 보관하므로, 여기서 잡히는 케이스는 같은 거래로는 회수 불가입니다 — 이 계측의 목적은 회수가 아니라 원인 분해입니다.

이벤트 / 컨텍스트읽는 법
siv_native_webview_process_goneWebView 렌더러(Android)/콘텐츠 프로세스(iOS)가 죽음. 표준창 거래 상태(userToken)는 이때 소멸awayForMs 가 있으면 인증앱 왕복 중 죽은 것. 백그라운드 emit 은 유실될 수 있어 하한
siv_native_bounced_to_start직전 URL 이 표준창(*.ez-iok.com)이었고, 결과 파라미터 없이 SIV 페이지로 넘어옴sinceReturnMs 가 수백 ms 면 표준창 스크립트가 history.back() 한 것, 길면 사용자 행동. prevPath 로 어느 표준창 페이지였는지
siv_native_hardware_backAndroid 뒤로가기(소비하지 않음, 관측만)lastUrlKind:eziok 이면 표준창 위에서 이탈
siv_native_returned_from_external_applastUrlKind · lastUrlPath · webviewProcessGone복귀 순간 WebView 위치와 왕복 중 프로세스 종료 여부lastUrlKind:app 이면 돌아오기 전에 이미 우리 페이지로 되돌려진 것
siv_resumed_after_process_killjsUptimeMsJS 번들이 살아 있던 시간수 초 이하면 콜드스타트(프로세스 킬), 길면 Activity 재생성 등 warm 재마운트

URL 은 호스트·경로만 남기고 쿼리는 버립니다 — 표준창 쿼리에는 거래 토큰이, SIV 페이지 쿼리에는 복호화된 실명(eziokResult)이 실립니다. other(임의 사이트)는 경로도 남기지 않습니다.

자동 채우기 효과 추적 (2026-09-10 전면 적용)

표준창 자동 채우기(Auto fill-in)를 2026-09-10 16:2x KST 에 프로덕션 전면 적용했습니다(ADR-0008). 목적은 미도달의 가장 큰 구간 — 인증앱에 도달하기도 전에 이탈하는 버킷 — 을 줄이는 것입니다.

⚠️ 지표 정의가 두 번 바뀌었습니다 (2026-09-13 정정)

처음 적은 정의에 결함이 있어 바로잡았습니다. 아래 정정본을 쓰세요. 옛 정의로 낸 숫자(1차 지표 71.5%)는 유입 구성이 바뀌면 실제 변화 없이 움직입니다.

무엇이 틀렸나
1차 지표에 deviceBrowser 조건이 없었다siv_native_external_app_opennative 셸 전용이다. 문자 링크를 인앱 브라우저로 연 사용자는 분모에만 들어가 비율을 끌어내린다
사용자 단위 비율을 다른 길이의 창끼리 비교했다카디널리티는 창이 길수록 재시도 사용자가 병합돼 비율이 올라간다. 같은 길이로만 비교할 수 있다

실제로 2026-09-11 문자 발송 때 이 결함이 드러났습니다 — 완료 사용자가 인증앱 도달 사용자보다 많아지는 모순이 나왔습니다.

동결 기준선 (적용 전)

창: 2026-09-01 ~ 09-07 (7일). 09-08 은 제외합니다. 그날 자동 채우기 1차 시도가 8분간 인증을 전면 중단시켜(시도 급증 + 결과 도달 0) 기준선이 아래로 편향됩니다. 09-10 도 적용 당일이라 제외합니다.

시도는 native(deviceBrowser:MWV)만 셉니다.

날짜시도(MWV)..._external_app_open전환율
09-01271866.7%
09-02302170.0%
09-03212095.2%
09-0436027175.3%
09-051168674.1%
09-061359570.4%
09-0724616767.9%
합계93267872.7%

2차 지표(종단 완료율)는 native·web 양쪽 다 유효하므로 전체로 셉니다 — 948 시도 중 siv_save_log_ok 507 = 53.5%.

지표 정의

분자·분모를 이벤트명과 필터까지 포함해 고정합니다.

지표정의기준선
1차 (인증앱 도달률)siv_native_external_app_open / (siv_auth_start @context.deviceBrowser:MWV)72.7%
2차 (종단 완료율, 시도 단위)siv_save_log_ok / siv_auth_start53.5%
2차 (종단 완료율, 사용자 단위)siv_save_log_ok / siv_auth_start@usr.id 카디널리티창 길이를 맞춰서 비교 (2일 창 09-06~07 = 91.6%)
프리필 적용률prefill_applied / request_issued (Vercel 로그)
통신사 도달률prefill_appliedfieldCount=4 비율

1차 지표가 이 작업의 대상입니다. 자동 채우기는 표준창 입력 구간만 건드리므로, 종단 완료율은 왕복 실패(프로세스 종료·결과 유실)에도 영향받아 신호가 희석됩니다.

판독 시점과 함정

  • 최소 7일 뒤에 판독합니다. 기준선 안에서도 일별 전환율이 66.7~95.2% 로 흔들립니다. 48시간으로는 노이즈와 신호를 구분할 수 없습니다

  • ⚠️ 대량 발송 코호트를 분리하세요. 동의서 패키지 문자 발송이 하루에 수백~천 명을 몰고 오며, 그 유입은 인앱 브라우저라 native 지표에 잡히지 않고 사용자 구성도 평시와 다릅니다. @view.url:*<packageId>* 로 코호트를 골라내 제외하거나 따로 봅니다

    실측(2026-09-1112): 일산 백송마을1235 통합재건축 동의서 패키지 (1b983cfe-dcef-40d5-b07a-1989727a8311) 하나가 974명·시도 1,803건으로 그 기간 전체 시도의 85% 를 차지했습니다. 같은 기간 deviceBrowser:MB(인앱 브라우저) 시도가 평시 17건/일에서 263건/일 로 뛰었습니다.

    이 정도 규모는 평균을 완전히 덮어씁니다. 발송이 있었는지 모른 채 지표만 보면 “적용 후 전환율 10%p 하락”이라는 잘못된 결론이 나옵니다(실제로 한 번 그렇게 읽었습니다).

  • ⚠️ siv_native_returned_from_external_app..._external_app_open 보다 많습니다. 복귀가 진입의 부분집합이 아니므로 차분으로 버킷을 계산하면 안 됩니다

  • 2026-09-10 에 웹뷰 정리 배치 A~E 가 함께 배포됐습니다. 지표가 크게 흔들리면 이 변경도 후보로 두세요

조회

# 1차 지표 — native 한정 (이 필터가 빠지면 유입 구성 변화에 오염된다) @type:action @action.name:siv_auth_start @context.deviceBrowser:MWV → 분모 @type:action @action.name:siv_native_external_app_open → 분자 # 2차 지표 (시도 단위) @type:action @action.name:(siv_auth_start OR siv_save_log_ok) → group by [@action.name], interval 86400000 # 대량 발송 코호트 분리 (packageId 는 SIV 화면 URL 에 들어 있다) @type:action @action.name:siv_auth_start @view.url:*<packageId>* # ⚠️ native 이벤트(external_app_open)는 view.url 이 native 화면이라 이 필터로 안 걸린다 # 유입 구성 확인 — MB(인앱 브라우저) 가 튀면 대량 발송이 있었다는 뜻 @type:action @action.name:siv_auth_start → group by @context.deviceBrowser, interval 86400000 # 프리필 적용률·통신사 도달률 (Vercel, 프로덕션 배포 URL 필요) vercel logs <prod-url> --scope howmuchhome --no-branch --query eziok --limit 500 --json # prefill_applied 의 fieldCount: 3 = 통신사 없음(은행 인증), 4 = 통신사 포함 # protocol_selected 의 version 이 v2 인지도 함께 확인 (강등되면 프리필이 안 나간다)

되돌릴 조건

EZIOK_PROTOCOL_VERSION 을 지우고 재배포하면 v1 으로 돌아갑니다(즉시 필요하면 이전 배포로 vercel promote — 2초). 아래 중 하나면 되돌립니다.

  • 종단 완료율이 기준선(53.5%) 대비 유의하게 하락
  • result_* 실패 이벤트나 표준창 오류코드가 새로 등장
  • protocol_downgraded 가 지속 발생 (v2 구성이 깨진 것)

핵심 지표는 siv_returned_without_result 입니다. “인증을 시작했는데 결과 파라미터 없이 화면이 다시 열렸다” = 결과 유실이고, 이 사건은 서버에 아무 요청도 남기지 않아 이 이벤트 없이는 관측이 불가능합니다. 표식을 sessionStorage 가 아니라 localStorage 에 두는 이유도 이것입니다 — 프로세스가 죽으면 WebView 가 파괴되어 sessionStorage 는 함께 사라지고, 정작 가장 알고 싶은 케이스를 놓칩니다.

native 쪽에서 배운 것 하나: 처음에는 외부 인증앱 딥링크를 가로채는 시점(onShouldStartLoadWithRequest)에 중단 복구 표식을 남겼는데, 계측을 붙여 보니 그 분기가 프로덕션에서 0건이었습니다(표준창이 핸들러를 우회해 앱을 띄움). 그래서 표식은 AppState 백그라운드 전환에서 남기고, 그 경로가 실제로 도는지 siv_native_backgrounded 로 확인합니다 — 계측이 없었다면 죽은 코드인 줄 몰랐을 사례입니다.

BFF 라우트는 route handler(Node 런타임)라 Logger 를 쓸 수 없습니다 — 브라우저 SDK 기반이고 Logger.isServer 가드로 전부 no-op 이 됩니다. 대신 Vercel 런타임 로그로 나가는 구조화 console 출력을 쓰고, 이렇게 조회합니다.

vercel logs -p howmuchhome-web-app --scope howmuchhome \ --environment production --no-branch --since 10h --limit 500 \ --query "[eziok]" --json

⚠️ 이 플로우의 로그에는 인증 명의(이름·생년월일)·encCi·hubToken 을 절대 넣지 않습니다. 표준창 결과의 data 는 복호화된 실명입니다. 남길 수 있는 건 거래 식별자(clientTxId)·인증기관(providerId)·상태·소요시간뿐입니다.

패키지 레퍼런스

자세한 API는 @howmuchhome-web/logger를 참고하세요.

Last updated on