Datadog RUM 기반 서비스 품질 개선
- 작업일: 2026-04-30
- PR: #4604
- 브랜치:
HMH-6689 - Baseline 문서: rum-baseline-2026-04-30.mdx
배경
Datadog RUM 30일(2026-03-30 ~ 2026-04-30) 데이터를 분석하여, 전체 유니크 세션 약 195K 기준으로 사용하지 않는 기능 식별, 에러 핫스팟 해결, 성능 개선, 사용성 개선을 진행했습니다.
분석에 사용한 Datadog 쿼리
| 용도 | 쿼리 |
|---|---|
| 페이지별 뷰 수 | aggregate_rum_events → @type:view group by @view.url_path |
| 유니크 세션 | aggregate_rum_events → CARDINALITY(@session.id) group by @view.url_path |
| 성능 (P50/P90) | aggregate_rum_events → @type:view @view.loading_time:>0 P50/P90 group by @view.url_path |
| 에러 수 | aggregate_rum_events → @type:error group by @view.url_path |
| 에러 패턴 | aggregate_rum_events → @type:error @view.url_path:/knowledge-space group by @error.message |
| 액션 빈도 | aggregate_rum_events → @type:action group by @action.name |
Phase 1: 에러 핫스팟 해결
1-1. touchmove intervention 에러 필터링
문제: 지식공간 에러율 68%, 문서보관함 에러율 19%로 보이지만 89~93%가 브라우저 intervention 경고
"Ignored attempt to cancel a touchmove event with cancelable=false"원인: 스크롤 중 preventDefault() 호출 시 브라우저가 남기는 passive 경고. 실제 사용자 영향 없음.
수정:
packages/logger/provider/client/ClientLogger.tsx— Datadog RUMbeforeSend에서cancelable=false및navigator.vibrate에러 필터링packages/ui/utils/components/PreventScrollWhenSlide.tsx— touchmove 이벤트에passive: false명시
검증 방법: 배포 후 Datadog RUM에서 /knowledge-space, /document-box 에러 수 확인
1-2. React Hydration Mismatch (React error #418)
문제: 커뮤니티 채팅에서 1,177건/7일, 문서보관함에서 50건/7일
원인 분석:
InputWrapperForIOS—isIOS()체크가 렌더 타임에 실행되어 서버(false) vs iOS 클라이언트(true) HTML 구조 불일치ChatroomMessageInput—isIOS()조건부로 hidden input 렌더링NoListingCafeSection—new Date().toJSON()이 모듈 스코프에서 평가되어 SSR/클라이언트 간 불일치
수정:
packages/ui/utils/components/InputWrapperForIOS.tsx—isIOS()체크를useEffect후로 지연 (hydration 완료 후 iOS 분기)ChatroomMessageInput/index.tsx— hidden input을 항상 렌더링 (tabIndex={-1},aria-hidden)NoListingCafeSection.tsx—new Date().toJSON()→ 고정 날짜 문자열'2026-04-01T12:00:00.000Z'
검증 방법: iOS 기기에서 채팅 진입 시 콘솔에 hydration 경고 없음 확인
1-3. Sendbird 연결 에러 안정화
문제 (7일 기준):
Command received no ack— 185건 (Sendbird 명령 타임아웃)Connection is required— 34건 (연결 끊긴 상태에서 작업 시도)
근본 원인:
isConnected()가currentUser !== null만 체크 → 소켓이 죽어도 연결된 것으로 판단- 자동 재연결 로직 없음
connect()에러를console.log로 삼킴 → 연결 실패해도isConnected=true
수정:
packages/chat/libs/classes/chat.ts:isConnected():connectionState === 'OPEN'체크 추가connect(): try/catch 제거하여 에러 전파addConnectionHandler()/removeConnectionHandler()메서드 추가
packages/chat-react/contexts/ChatConnectContext.tsx:- Sendbird
ConnectionHandler등록 (onReconnectStarted/Succeeded/Failed) onReconnectFailed시 수동connectIfNeeded()재시도connect()실패 시catch에서setIsConnected(false)
- Sendbird
검증 방법: 모바일에서 네트워크 끊김 → 재연결 시 채팅 자동 복구 확인
1-4. 비즈니스 로직 에러 처리
문제: QUIZ_ALREADY_SUBMITTED 104건/7일 (지식공간)
원인: handleAnswer에서 mutation 진행 중 중복 클릭 방지 없음
수정:
knowledge-space/page.tsx—quizSolveMutation.isPending+quiz?.response != null가드 추가
Phase 2: 성능 개선
2-1. 루트 페이지 API 병렬화
문제: P90 5.9초, P50 2.2초. 순차 API 호출 체인:
getMe() → getMyOnboardingStatus() → getHomeAccess() → getFirstUncertifiedListing()수정 (apps/app/src/app/page.tsx):
// Before: 순차 호출 (4 round trips)
const user = await UserAPI.getMe();
const certificationStatus = await UserAPI.getMyOnboardingStatus();
const homeAccess = await UserAPI.getHomeAccess();
const firstUncertifiedListing = await getFirstUncertifiedListing();
// After: 병렬 호출 (2 round trips)
const [user, certificationStatus] = await Promise.all([
UserAPI.getMe(),
UserAPI.getMyOnboardingStatus().catch(() => null),
]);
const [homeAccess, firstUncertifiedListing] = await Promise.all([
UserAPI.getHomeAccess(),
getFirstUncertifiedListing(),
]);예상 효과: ~300-400ms 절감 (API round trip 2개 절약)
2-2. 채팅 레이아웃 API 병렬화
문제: getChatByUrl이 단독 실행 후 → 6개 API 순차 배치. cafeId 의존성 때문에 2단계 waterfall.
수정 (community/(chat-server-connect)/[chatUrl]/layout.tsx):
// Before: getChatByUrl 단독 → 6개 API
const [chat] = await Promise.all([fetchAPIQuery(ChatAPI.getChatByUrl, {chatUrl})]);
const cafeId = chat.cafe.id;
await Promise.all([...6개 API...]);
// After: 비의존 4개를 getChatByUrl과 병렬 → cafeId 의존 2개만 2차 배치
const [chat] = await Promise.all([
fetchAPIQuery(ChatAPI.getChatByUrl, {chatUrl}),
fetchAPIQuery(ChatAPI.getChatParticipantsList, {chatUrl}), // cafeId 불필요
fetchAPIQuery(UserAPI.getMyProfile, {chatUrl}), // cafeId 불필요
fetchAPIQuery(ChatAPI.getMyChatByUrl, {chatUrl}), // cafeId 불필요
fetchAPIQuery(UserAPI.getMyChatList), // cafeId 불필요
]);
const cafeId = chat.cafe.id;
await Promise.all([
fetchAPIQuery(CafeAPI.getCafeActiveNotice, {cafeId}),
fetchAPIQuery(CafeAPI.getUnreadPostsCount, {cafeId}),
]);예상 효과: ~300ms 절감 (1차 배치에서 4개 API 동시 실행)
2-3. /vote 레거시 서버 리다이렉트
문제: P90 4.7초. 클라이언트 useEffect → router.push로 전체 JS 로딩 후 리다이렉트.
수정 (apps/app/src/app/vote/page.tsx):
// Before: 클라이언트 리다이렉트
'use client';
useEffect(() => { router.push(getRoutePath('home')); }, []);
// After: 서버 리다이렉트
import { redirect } from 'next/navigation';
redirect(getRoutePath('home'));예상 효과: P90 4.7초 → ~0.5초 (클라이언트 JS 로딩 생략)
Phase 3: 사용성 개선
3-1. 서명 UX 개선
문제: “다시 서명하기” 155,844회 vs “다음으로” 171,406회 = 47.6% 재시도율
원인 분석:
hasSignature가startDrawing즉시 true → 아주 작은 터치도 서명으로 인정- “다시 서명하기” 버튼이 서명 후에만 표시 → 사용자가 처음에 버튼 존재를 모름
- 이름 워터마크가 서명 시작 시 사라짐 → 가이드 없이 서명
수정:
_hooks/useSignatureCanvas.ts:strokeLengthRef로 스트로크 길이 추적- 최소 30px 이상 그려야
hasSignature=true(실수 터치 방지) hasSignature설정을startDrawing→stopDrawing으로 이동
_components/SignatureCanvas.tsx:- “다시 서명하기” 버튼 항상 표시 (
disabled={!hasSignature}) - 이름 워터마크를 서명 중에도 유지 (“여기에 서명해주세요” 텍스트만 숨김)
onSignatureChange를useEffect로hasSignature변경 감지
- “다시 서명하기” 버튼 항상 표시 (
검증 방법: 배포 후 “다시 서명하기” / “다음으로” 비율 변화 추적
3-2. 채팅↔카페 탭 전환 최적화
문제: 탭 전환 2,530회/일, 매번 34초 full page reload
원인: router.replace()로 전체 라우트 변경 → 서버 RSC 재렌더링 + API 재호출
수정 (CommunityHeader.tsx):
// 다른 탭 데이터를 미리 로드
useEffect(() => {
if (currentPage === 'chat') {
router.prefetch(cafePath);
} else {
router.prefetch(chatPath);
}
}, [chatUrl, currentPage, router]);예상 효과: Next.js가 다른 탭의 RSC payload를 미리 캐시 → 전환 시 1~2초로 감소
향후 개선 방향: parallel routes (@chat, @cafe) 구조로 전환하면 <500ms 가능
3-3. 세무상담 노출 강화
문제: 유니크 세션 2,102 (전체의 1.1%). 최근 릴리즈된 기능이지만 홈에서 8번째 위치.
수정 (Home.tsx):
Before: HomeDocuments → HomeAdvertisement → HomeQuiz → HomeCommunityList → HomeShortcut
After: HomeDocuments → HomeShortcut → HomeAdvertisement → HomeQuiz → HomeCommunityList예상 효과: 스크롤 없이 바로가기(세무상담 포함) 노출 → 유니크 세션 증가
검증 계획
배포 후 1~2주 뒤 아래 Datadog 쿼리로 before/after 비교:
| 지표 | Before (baseline) | 쿼리 |
|---|---|---|
| 지식공간 에러율 | 68.3% | @type:error @view.url_path:/knowledge-space |
| 문서보관함 에러율 | 18.9% | @type:error @view.url_path:/document-box |
| 채팅 hydration 에러 | 1,177/주 | @type:error @error.message:*418* @view.url_path:/community/*/chat |
| Sendbird no ack | 185/주 | @type:error @error.message:*no ack* |
| 루트 P90 | 5.86s | @type:view @view.url_path:/ P90 @view.loading_time |
| 채팅 P50 | 3.55s | @type:view @view.url_path:/community/*/chat P50 @view.loading_time |
| 투표 P90 | 4.70s | @type:view @view.url_path:/vote P90 @view.loading_time |
| 서명 재시도율 | 47.6% | @type:action @action.name:*서명* 비율 |
| 세무상담 유니크 세션 | 2,102 | @type:view @view.url_path:/tax-chat CARDINALITY @session.id |
변경된 파일 목록
| 파일 | Phase | 변경 내용 |
|---|---|---|
packages/logger/provider/client/ClientLogger.tsx | 1-1 | RUM beforeSend 에러 필터링 |
packages/ui/utils/components/PreventScrollWhenSlide.tsx | 1-1 | passive 옵션 명시 |
packages/ui/utils/components/InputWrapperForIOS.tsx | 1-2 | isIOS() hydration 후 지연 |
ChatroomMessageInput/index.tsx | 1-2 | hidden input 항상 렌더링 |
NoListingCafeSection.tsx | 1-2 | 목업 날짜 고정 |
packages/chat/libs/classes/chat.ts | 1-3 | isConnected 소켓 체크, ConnectionHandler |
packages/chat-react/contexts/ChatConnectContext.tsx | 1-3 | 자동 재연결, 에러 핸들링 |
knowledge-space/page.tsx | 1-4 | 퀴즈 중복 제출 방지 |
apps/app/src/app/page.tsx | 2-1 | API 병렬화 |
community/(chat-server-connect)/[chatUrl]/layout.tsx | 2-2 | API 병렬화 |
apps/app/src/app/vote/page.tsx | 2-3 | 서버 리다이렉트 |
vote/[voteId]/_hooks/useSignatureCanvas.ts | 3-1 | 최소 스트로크 검증 |
vote/[voteId]/_components/SignatureCanvas.tsx | 3-1 | 버튼 항상 표시, 워터마크 유지 |
CommunityHeader.tsx | 3-2 | router.prefetch 추가 |
home/v2/Home.tsx | 3-3 | HomeShortcut 위치 상향 |
rum-baseline-2026-04-30.mdx | - | 개선 전 기준 데이터 |