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

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 애플리케이션의 프론트엔드 모니터링을 위한 강력하고 유연한 도구입니다. 적절한 설정과 활용을 통해 사용자 경험을 개선하고 애플리케이션의 안정성을 높일 수 있습니다.

단계적으로 도입하여 팀의 모니터링 역량을 점진적으로 향상시키는 것을 권장합니다. 처음에는 기본적인 에러 추적부터 시작하여, 점차 커스텀 이벤트와 성능 메트릭을 추가해 나가세요.