Grafana Faro로 React/TypeScript 프론트엔드 모니터링하기

프론트엔드 애플리케이션의 성능과 사용자 경험을 실시간으로 모니터링하는 것은 현대 웹 개발에서 필수적입니다. Grafana Faro는 실시간 사용자 모니터링(RUM, Real User Monitoring)을 위한 오픈소스 웹 SDK로, 프론트엔드 애플리케이션의 성능, 에러, 사용자 행동을 효과적으로 추적할 수 있게 해줍니다.
이 글에서는 React와 TypeScript 환경에서 Grafana Faro를 설정하고 활용하는 방법을 단계별로 알아보겠습니다.
Grafana Faro의 주요 기능
- 실시간 에러 트래킹: JavaScript 에러와 예외 상황을 실시간으로 수집
- 성능 모니터링: 페이지 로딩 시간, 리소스 사용량 등 성능 지표 추적
- 사용자 세션 추적: 사용자의 클릭, 네비게이션 등 행동 패턴 분석
- 커스텀 이벤트: 비즈니스 로직에 맞는 커스텀 메트릭 수집
- Grafana 대시보드 연동: 수집된 데이터를 시각화하여 모니터링
1. 설치 및 초기 설정
패키지 설치
먼저 Grafana Faro Web SDK를 설치합니다:
npm install @grafana/faro-web-sdk
# 또는
yarn add @grafana/faro-web-sdk
기본 설정 파일 생성
프로젝트 root에 faro.config.ts
파일을 생성합니다:
// faro.config.ts
import { getWebInstrumentations, initializeFaro } from '@grafana/faro-web-sdk';
export const initFaro = () => {
return initializeFaro({
url: process.env.REACT_APP_FARO_URL || 'http://localhost:12347/collect',
app: {
name: 'my-react-app',
version: '1.0.0',
environment: process.env.NODE_ENV,
},
instrumentations: [
...getWebInstrumentations({
captureConsole: true,
captureConsoleDisabledLevels: [],
}),
],
});
};
2. React 애플리케이션에 통합
App.tsx에서 Faro 초기화
// App.tsx
import React, { useEffect } from 'react';
import { initFaro } from './faro.config';
import { faro } from '@grafana/faro-web-sdk';
function App() {
useEffect(() => {
// 개발 환경에서는 Faro를 비활성화할 수 있습니다
if (process.env.NODE_ENV === 'production') {
initFaro();
}
}, []);
return (
<div className="App">
{/* 앱 컴포넌트들 */}
</div>
);
}
export default App;
환경 변수 설정
.env
파일에 Faro 수집 서버 URL을 설정합니다:
# .env
REACT_APP_FARO_URL=https://your-faro-collector.example.com/collect
3. 에러 바운더리와 연동
React의 에러 바운더리와 Faro를 연동하여 컴포넌트 에러를 추적할 수 있습니다:
// components/ErrorBoundary.tsx
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { faro } from '@grafana/faro-web-sdk';
interface Props {
children: ReactNode;
}
interface State {
hasError: boolean;
}
class ErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false
};
public static getDerivedStateFromError(_: Error): State {
return { hasError: true };
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Uncaught error:', error, errorInfo);
// Faro로 에러 전송
faro.api.pushError(error, {
context: {
errorInfo: JSON.stringify(errorInfo),
timestamp: new Date().toISOString(),
}
});
}
public render() {
if (this.state.hasError) {
return <h1>죄송합니다. 문제가 발생했습니다.</h1>;
}
return this.props.children;
}
}
export default ErrorBoundary;
4. 커스텀 이벤트 및 메트릭 추적
사용자 행동 추적
// hooks/useFaroTracking.ts
import { faro } from '@grafana/faro-web-sdk';
import { useCallback } from 'react';
export const useFaroTracking = () => {
const trackEvent = useCallback((eventName: string, properties?: Record<string, any>) => {
faro.api.pushEvent(eventName, properties, 'interaction');
}, []);
const trackPageView = useCallback((pageName: string) => {
faro.api.pushEvent('page_view', {
page: pageName,
timestamp: new Date().toISOString(),
}, 'page');
}, []);
const trackUserAction = useCallback((action: string, element?: string) => {
faro.api.pushEvent('user_action', {
action,
element,
timestamp: new Date().toISOString(),
}, 'interaction');
}, []);
return { trackEvent, trackPageView, trackUserAction };
};
컴포넌트에서 활용
// components/ProductCard.tsx
import React from 'react';
import { useFaroTracking } from '../hooks/useFaroTracking';
interface Product {
id: string;
name: string;
price: number;
}
const ProductCard: React.FC<{ product: Product }> = ({ product }) => {
const { trackUserAction } = useFaroTracking();
const handleAddToCart = () => {
// 비즈니스 로직
addToCart(product.id);
// Faro로 이벤트 추적
trackUserAction('add_to_cart', `product_${product.id}`);
};
const handleProductView = () => {
trackUserAction('product_view', `product_${product.id}`);
};
return (
<div className="product-card" onClick={handleProductView}>
<h3>{product.name}</h3>
<p>${product.price}</p>
<button onClick={handleAddToCart}>
장바구니에 추가
</button>
</div>
);
};
5. 성능 모니터링
커스텀 타이밍 측정
// utils/performanceUtils.ts
import { faro } from '@grafana/faro-web-sdk';
export const measureAsyncOperation = async <T>(
operationName: string,
operation: () => Promise<T>
): Promise<T> => {
const startTime = performance.now();
try {
const result = await operation();
const duration = performance.now() - startTime;
// 성공한 작업의 수행 시간 기록
faro.api.pushMeasurement({
type: 'custom',
name: `${operationName}_duration`,
value: duration,
unit: 'milliseconds',
});
return result;
} catch (error) {
const duration = performance.now() - startTime;
// 실패한 작업도 시간과 함께 에러 기록
faro.api.pushError(error as Error, {
context: {
operation: operationName,
duration: duration.toString(),
}
});
throw error;
}
};
API 호출 모니터링
// services/api.ts
import { measureAsyncOperation } from '../utils/performanceUtils';
export const fetchUserData = async (userId: string) => {
return measureAsyncOperation(
'fetch_user_data',
async () => {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
}
);
};
6. React Router와 연동
페이지 네비게이션을 자동으로 추적하는 방법:
// hooks/useRouteTracking.ts
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { useFaroTracking } from './useFaroTracking';
export const useRouteTracking = () => {
const location = useLocation();
const { trackPageView } = useFaroTracking();
useEffect(() => {
trackPageView(location.pathname);
}, [location.pathname, trackPageView]);
};
// App.tsx에서 사용
import { useRouteTracking } from './hooks/useRouteTracking';
function App() {
useRouteTracking(); // 라우트 변경 시 자동 추적
return (
<Router>
<Routes>
{/* 라우트 정의 */}
</Routes>
</Router>
);
}
7. 사용자 컨텍스트 설정
사용자 정보를 Faro에 설정하여 더 상세한 분석이 가능합니다:
// hooks/useUserContext.ts
import { useEffect } from 'react';
import { faro } from '@grafana/faro-web-sdk';
export const useUserContext = (user?: { id: string; email: string; role: string }) => {
useEffect(() => {
if (user) {
faro.api.setUser({
id: user.id,
email: user.email,
attributes: {
role: user.role,
},
});
} else {
// 로그아웃 시 사용자 정보 제거
faro.api.setUser({
id: '',
email: '',
attributes: {},
});
}
}, [user]);
};
8. 고급 설정
샘플링 설정
트래픽이 많은 경우 데이터 수집을 제한할 수 있습니다:
// faro.config.ts 업데이트
export const initFaro = () => {
return initializeFaro({
url: process.env.REACT_APP_FARO_URL || 'http://localhost:12347/collect',
app: {
name: 'my-react-app',
version: '1.0.0',
environment: process.env.NODE_ENV,
},
sessionTracking: {
enabled: true,
samplingRate: 0.1, // 10%만 추적
},
instrumentations: [
...getWebInstrumentations({
captureConsole: true,
captureConsoleDisabledLevels: ['debug'],
}),
],
beforeSend: (event) => {
// 민감한 정보 필터링
if (event.type === 'log' && event.context?.message?.includes('password')) {
return null; // 이벤트 전송 차단
}
return event;
},
});
};
커스텀 메타데이터 추가
// utils/faroMetadata.ts
import { faro } from '@grafana/faro-web-sdk';
export const setGlobalMetadata = () => {
faro.api.setGlobalMetadata({
browser: {
name: navigator.userAgent,
language: navigator.language,
viewport: `${window.innerWidth}x${window.innerHeight}`,
},
app: {
buildTime: process.env.REACT_APP_BUILD_TIME,
gitCommit: process.env.REACT_APP_GIT_COMMIT,
},
});
};
9. Grafana 대시보드 설정
수집된 데이터를 시각화하기 위한 기본 대시보드 쿼리 예시:
에러율 추적
rate(faro_exceptions_total[5m]) * 100
페이지 로딩 시간
histogram_quantile(0.95, rate(faro_page_load_duration_bucket[5m]))
사용자 액션 빈도
rate(faro_events_total{event_type="interaction"}[1m])
10. 모범 사례
성능 최적화
- 프로덕션 환경에서만 Faro 활성화
- 샘플링을 통한 데이터 수집량 조절
- 민감한 정보는 beforeSend에서 필터링
에러 처리
- 모든 비동기 작업에 에러 처리 추가
- 사용자에게 영향을 주지 않도록 Faro 초기화 실패 처리
데이터 품질
- 의미있는 이벤트명과 속성명 사용
- 일관된 네이밍 컨벤션 적용
- 불필요한 이벤트 전송 방지
결론
Grafana Faro는 React/TypeScript 애플리케이션의 프론트엔드 모니터링을 위한 강력하고 유연한 도구입니다. 적절한 설정과 활용을 통해 사용자 경험을 개선하고 애플리케이션의 안정성을 높일 수 있습니다.
단계적으로 도입하여 팀의 모니터링 역량을 점진적으로 향상시키는 것을 권장합니다. 처음에는 기본적인 에러 추적부터 시작하여, 점차 커스텀 이벤트와 성능 메트릭을 추가해 나가세요.