내비게이션

@sophonz/react-native-navigation으로 expo-router, React Navigation, react-native-navigation의 화면마다 ux.view 스팬을 기록하는 방법 — 설정, 스팬 이름과 속성, 앱 상태 처리, 옵션을 설명합니다.

@sophonz/react-native-navigation은 화면 전환을 스팬으로 바꿉니다. 이미 쓰고 있는 내비게이션 라이브러리를 구독해 라우트가 바뀌면 이전 화면의 스팬을 끝내고 새 라우트의 스팬을 시작하므로, 세션 타임라인에서 어떤 일이 일어났을 때 사용자가 어느 화면에 있었는지 볼 수 있습니다. 패키지는 순수 JavaScript이며 tracer provider를 통해 기록합니다.

설치

npm install @sophonz/react-native-navigation@0.1.0 @sophonz/react-native-tracer-provider@0.1.0
라이브러리컴포넌트넘길 ref
expo-routerSophonzNavigationTrackerexpo-router의 useNavigationContainerRef()
@react-navigation/nativeSophonzNavigationTrackeruseRef(useNavigationContainerRef())
react-native-navigationSophonzNativeNavigationTrackeruseRef(Navigation.events())

@react-navigation/native와 react-native-navigation은 선택적 peer dependency입니다. 쓰는 것만 설치하세요.

expo-router

app/_layout.tsx에서 루트 스택을 감쌉니다. 첫 화면의 스팬을 놓치지 않도록 tracer provider가 준비된 뒤에 트래커를 렌더링하세요.

app/_layout.tsx
import {Stack, useNavigationContainerRef} from "expo-router";
import {useSophonz} from "@sophonz/react-native";
import {useSophonzNativeTracerProvider} from "@sophonz/react-native-tracer-provider";
import {SophonzNavigationTracker} from "@sophonz/react-native-navigation";
 
// 설정 플러그인을 쓰는 Expo는 SDK를 네이티브에서 시작하지만, 이 옵션은 여전히 적용됩니다
const SOPHONZ_CONFIG = {trackUnhandledRejections: true};
 
export default function RootLayout() {
  const {isStarted} = useSophonz(SOPHONZ_CONFIG);
  const {tracerProvider} = useSophonzNativeTracerProvider(undefined, isStarted);
  const navigationRef = useNavigationContainerRef();
 
  if (!tracerProvider) {
    return null; // 또는 스플래시 화면
  }
 
  return (
    <SophonzNavigationTracker
      ref={navigationRef}
      tracerProvider={tracerProvider}
      screenAttributes={{"app.flavor": "production"}}>
      <Stack>
        <Stack.Screen name="(tabs)" options={{headerShown: false}} />
        <Stack.Screen name="+not-found" />
      </Stack>
    </SophonzNavigationTracker>
  );
}

React Navigation

@react-navigation/native의 useNavigationContainerRef는 ref가 아니라 내비게이션 객체 자체를 돌려줍니다. 그 객체는 NavigationContainer에, 그 객체를 담은 useRef는 트래커에 넘기세요.

App.tsx
import {useRef} from "react";
import {NavigationContainer, useNavigationContainerRef} from "@react-navigation/native";
import {createNativeStackNavigator} from "@react-navigation/native-stack";
import {useSophonz} from "@sophonz/react-native";
import {useSophonzNativeTracerProvider} from "@sophonz/react-native-tracer-provider";
import {SophonzNavigationTracker} from "@sophonz/react-native-navigation";
 
const Stack = createNativeStackNavigator();
const SOPHONZ_CONFIG = {trackUnhandledRejections: true};
 
export default function App() {
  const {isStarted} = useSophonz(SOPHONZ_CONFIG);
  const {tracerProvider} = useSophonzNativeTracerProvider(undefined, isStarted);
 
  const navigation = useNavigationContainerRef();
  const trackerRef = useRef(navigation);
 
  if (!tracerProvider) {
    return null;
  }
 
  return (
    <NavigationContainer ref={navigation}>
      <SophonzNavigationTracker ref={trackerRef} tracerProvider={tracerProvider}>
        <Stack.Navigator>
          <Stack.Screen name="Cart" component={CartScreen} />
          <Stack.Screen name="Checkout" component={CheckoutScreen} />
        </Stack.Navigator>
      </SophonzNavigationTracker>
    </NavigationContainer>
  );
}

react-native-navigation

SophonzNativeNavigationTracker는 Navigation.events()의 componentDidAppear와 componentDidDisappear를 구독합니다. 이 이벤트는 전역이므로 모든 화면이 아니라 계속 등록되어 있는 컴포넌트 하나(보통 첫 화면)만 감싸세요. 여러 곳을 감싸면 감싼 수만큼 리스너가 추가됩니다.

index.tsx
import {useRef} from "react";
import {Navigation} from "react-native-navigation";
import {initialize} from "@sophonz/react-native";
import {SophonzNativeTracerProvider} from "@sophonz/react-native-tracer-provider";
import {SophonzNativeNavigationTracker} from "@sophonz/react-native-navigation";
 
const start = async () => {
  await initialize({sdkConfig: {trackUnhandledRejections: true}});
 
  // provider를 만들기 전에 SDK가 실행 중이어야 합니다
  const tracerProvider = new SophonzNativeTracerProvider();
 
  Navigation.registerComponent(
    "Home",
    () => props => {
      const events = useRef(Navigation.events());
      return (
        <SophonzNativeNavigationTracker ref={events} tracerProvider={tracerProvider}>
          <HomeScreen {...props} />
        </SophonzNativeNavigationTracker>
      );
    },
    () => HomeScreen,
  );
 
  Navigation.events().registerAppLaunchedListener(() => {
    Navigation.setRoot({root: {stack: {children: [{component: {name: "Home"}}]}}});
  });
};
 
start();

기록 내용

화면 방문마다 스팬 하나입니다.

필드값
스팬 이름라우트 이름(getCurrentRoute().name). react-native-navigation은 컴포넌트 이름
spz.typeux.view. screenAttributes에 spz.type을 넣어도 항상 이 값이 우선합니다
view.name쿼리용으로 한 번 더 넣는 라우트 이름
view.launch트래커가 마운트된 뒤 첫 화면이면 true, 이후는 false
view.state.end스팬이 끝날 때의 앱 상태. 일반 화면 이동이면 active, 앱이 포그라운드를 떠났다면 background나 inactive
view.unmount트래커가 언마운트되어 끝난 스팬이면 true
사용자 속성screenAttributes의 모든 값

화면 스팬은 라우트 이름이 바뀔 때 시작합니다. 같은 라우트로 파라미터만 바꿔 이동하거나 다시 렌더링하는 것으로는 새 스팬이 생기지 않습니다.

expo-router에서 라우트 이름은 URL이 아니라 라우터가 알려 주는 파일 기반 이름(index, (tabs) 등)입니다. 동적 라우트를 묶어 보고 싶다면 원하는 값을 screenAttributes에 넣거나 커스텀 스팬을 기록하세요.

앱 상태

트래커는 AppState도 따라갑니다.

  • 앱이 background나 inactive가 되면 현재 화면의 스팬이 끝나고 view.state.end에 그 상태가 들어갑니다.
  • active로 돌아오면 같은 화면의 새 스팬이 view.launch: false로 시작합니다.

그래서 다른 앱에 다녀오느라 중간에 끊긴 화면 방문은 스팬 두 개로 보입니다. iOS에서는 제어 센터나 전화 수신 배너 같은 시스템 오버레이 때문에도 잠깐 inactive가 됩니다.

옵션

속성타입기본값설명
refref필수설치의 표 참고
tracerProviderTracerProvider전역 provider기록에 쓸 provider. Sophonz provider를 넘기거나, 먼저 전역으로 등록하세요
screenAttributesAttributes{}모든 화면 스팬에 추가됩니다
tracerOptionsTracerOptions없음getTracer에 전달됩니다. schemaUrl용
debugbooleantrue트래커 동작을 [Sophonz] 접두어로 콘솔에 출력합니다. 프로덕션 빌드에서는 false로 두세요
childrennode필수내비게이터

tracer 이름은 @sophonz/react-native-navigation, 버전은 0.1.0입니다. iOS에서는 tracer 이름이 기록되지 않습니다.

네이티브 화면 수집

네이티브 SDK는 자체적으로 네이티브 화면을 기록합니다. Android에서는 보통 MainActivity 하나, iOS에서는 내비게이션 라이브러리가 만든 뷰 컨트롤러입니다. 이 패키지를 쓰면 그 스팬이 라우트 스팬과 겹치거나 어긋납니다. 라우트 스팬만 보고 싶다면 네이티브 수집을 끄세요.

android/app/src/main/sophonz-config.json
{
  "sdk_config": {
    "view_config": {"enable_automatic_activity_capture": false}
  }
}
// iOS, JavaScript가 SDK를 시작하는 경우
await initialize({sdkConfig: {ios: {collectorUrl: "https://in.sophonz.ai", appKey: "sk_ios_replace_me", disableAutomaticViewCapture: true}}});

iOS에서 SDK를 네이티브로 시작한다면 SophonzInitializer.swift에서 서비스를 제거합니다.

let services = CaptureServicesOptionsBuilder()
  .addDefaults()
  .remove(ofType: ViewCaptureService.self)
  .build()
 
try Sophonz.start(
  options: .withCollector(
    url: "https://in.sophonz.ai",
    appKey: "sk_ios_replace_me",
    platform: .reactNative,
    captureServices: services
  )
)

화면이 기록되지 않을 때

debug를 켜 두면 콘솔에 이유가 나옵니다.

  • Navigation ref is not available — 트래커가 렌더링될 때 ref에 값이 없었습니다. React Navigation에서는 내비게이션 객체 자체가 아니라 useRef(useNavigationContainerRef())를 넘기세요.
  • No TracerProvider found. Using global tracer instead. — tracerProvider 속성도 없고 trace.setGlobalTracerProvider로 등록한 것도 없습니다. 스팬은 no-op tracer로 갑니다.
  • no tracer available, not creating a span — provider가 생기기 전에 트래커가 렌더링됐습니다. tracerProvider가 준비된 뒤에만 렌더링하세요.