๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
React-Native

React Native์—์„œ OpenTelemetry ์‚ฌ์šฉํ•˜๊ธฐ, OTEL Web SDK๋กœ ๊ตฌํ˜„ํ•ด๋ณด์ž

by LasBe 2026. 3. 4.
๋ฐ˜์‘ํ˜•

๐Ÿ“’ React Native์—์„œ OpenTelemetry ์‚ฌ์šฉํ•˜๊ธฐ, OTEL Web SDK๋กœ ๊ตฌํ˜„ํ•ด๋ณด์ž


React Native ์•ฑ์˜ ์˜ต์ €๋นŒ๋ฆฌํ‹ฐ(Observability)๋ฅผ ๋†’์ด๊ณ  ์‹ถ์–ด์„œ OpenTelemetry(OTEL)๋ฅผ ์ฐพ์•„๋ดค์Šต๋‹ˆ๋‹ค.

 

๋ฌธ์„œ๋„ ์ฝ๊ณ , SDK ๋ชฉ๋ก๋„ ์‚ดํŽด๋ดค๋Š”๋ฐ, ์—†์Šต๋‹ˆ๋‹ค. RN ์ „์šฉ SDK๊ฐ€.

 

Web SDK๋Š” ์žˆ๊ณ , Node SDK๋Š” ์žˆ๊ณ , iOS/Android ๋„ค์ดํ‹ฐ๋ธŒ SDK๋Š” ์žˆ๋Š”๋ฐ, React Native๋ฅผ ์œ„ํ•œ JavaScript SDK๋Š” ๊ณต์‹์ ์œผ๋กœ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

 

๊ทธ๋ž˜์„œ Web SDK๋ฅผ RN ์œ„์—์„œ ๋Œ๋ฆฌ๊ธฐ๋กœ ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

์ƒ๊ฐ๋ณด๋‹ค ์ž˜ ๋ฉ๋‹ˆ๋‹ค. ๋‹จ, metro ๋ฒˆ๋“ค๋Ÿฌ๊ฐ€ ํ˜‘์กฐํ•˜๊ธฐ ์ „๊นŒ์ง€๋Š”์š”.

 

ํ™˜๊ฒฝ ์ •๋ณด

  • React Native 0.80.2
  • @opentelemetry/sdk-trace-web 2.0.0
  • @opentelemetry/instrumentation-fetch 0.200.0
  • @opentelemetry/exporter-trace-otlp-http 0.200.0
  • @opentelemetry/resources 2.0.1

 

๐Ÿ“Œ ์™œ ์˜ต์ €๋นŒ๋ฆฌํ‹ฐ์ธ๊ฐ€, ์™œ OTEL์ธ๊ฐ€

์•ฑ์ด ์ฃฝ์œผ๋ฉด ์••๋‹ˆ๋‹ค. Firebase Crashlytics๊ฐ€ ์•Œ๋ ค์ฃผ๋‹ˆ๊นŒ์š”.

 

๊ทธ๋Ÿฐ๋ฐ "์ฃฝ๊ธฐ ์ „์— ๋ญ˜ ํ•˜๊ณ  ์žˆ์—ˆ๋Š”์ง€"๋Š” ๋ชจ๋ฆ…๋‹ˆ๋‹ค.

 

API ์‘๋‹ต์ด ๋А๋ฆฐ ๊ฑด์ง€, ์–ด๋–ค ํ™”๋ฉด์—์„œ ์‚ฌ์šฉ์ž๊ฐ€ ์ดํƒˆํ–ˆ๋Š”์ง€, ํŠน์ • ๊ธฐ๋Šฅ์ด ์‹ค์ œ๋กœ ์–ผ๋งˆ๋‚˜ ์ž์ฃผ ํ˜ธ์ถœ๋˜๋Š”์ง€, ์ด๋Ÿฐ ๊ฒƒ๋“ค์€ ํฌ๋ž˜์‹œ ๋กœ๊ทธ๋กœ ์žกํžˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

 

์˜ต์ €๋นŒ๋ฆฌํ‹ฐ๊ฐ€ ํ•„์š”ํ–ˆ๋˜ ์ด์œ ์ž…๋‹ˆ๋‹ค.

 

๋‹จ์ˆœํ•œ ์—๋Ÿฌ ์ถ”์ ์„ ๋„˜์–ด, ์•ฑ ๋‚ด๋ถ€์—์„œ ๋ฌด์Šจ ์ผ์ด ๋ฒŒ์–ด์ง€๋Š”์ง€ "๋ณด๊ณ " ์‹ถ์—ˆ์Šต๋‹ˆ๋‹ค.

 

OpenTelemetry๋ฅผ ์„ ํƒํ•œ ์ด์œ ๋Š” ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค.

 

๋ฒค๋” ์ค‘๋ฆฝ์ ์ธ ํ‘œ์ค€์ด๋ผ, ์‹œ์žฅ์˜ ์ฃผ์š” ๋ชจ๋‹ˆํ„ฐ๋ง ์„œ๋น„์Šค๋“ค์ด OTEL์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.

 

๋•๋ถ„์— ์–ด๋–ค ์„œ๋น„์Šค๋ฅผ ์“ฐ๋”๋ผ๋„ SDK ์ฝ”๋“œ๋ฅผ ๊ทธ๋Œ€๋กœ ์œ ์ง€ํ•˜๋ฉด์„œ ๋ชจ๋‹ˆํ„ฐ๋ง ํˆด๋งŒ ์„ค์ •์œผ๋กœ ๊ต์ฒดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

๋ฌธ์ œ๋Š” RN ๊ณต์‹ SDK๊ฐ€ ์—†๋‹ค๋Š” ์ ์ด์—ˆ์Šต๋‹ˆ๋‹ค.

 

CNCF ํ”„๋กœ์ ํŠธ ํŽ˜์ด์ง€์— JavaScript SDK๋Š” ์žˆ์ง€๋งŒ, ์ด๊ฑด ๋ธŒ๋ผ์šฐ์ €์™€ Node ํ™˜๊ฒฝ์šฉ์ž…๋‹ˆ๋‹ค.

 

RN์€ ๋‘˜ ๋‹ค ์•„๋‹™๋‹ˆ๋‹ค. ์ •ํ™•ํžˆ๋Š” ๋‘˜ ๋‹ค์˜ ์ค‘๊ฐ„ ์–ด๋”˜๊ฐ€์ฃ .

 

๐Ÿ“Œ OTEL ํ•ต์‹ฌ ๊ฐœ๋…

SDK๋ฅผ ์ง์ ‘ ๋งŒ๋“ค๊ธฐ ์ „์— ํ•ต์‹ฌ ๊ฐœ๋… ์„ธ ๊ฐ€์ง€๋ฅผ ์งš๊ณ  ๋„˜์–ด๊ฐ€๊ฒ ์Šต๋‹ˆ๋‹ค.

 

๐Ÿ”Ž Trace์™€ Span (์ƒ๋ช…์ฃผ๊ธฐ)

Trace๋Š” ํ•˜๋‚˜์˜ ์š”์ฒญ์ด ์‹œ์Šคํ…œ์„ ํ†ต๊ณผํ•˜๋Š” ์ „์ฒด ์—ฌ์ •์ž…๋‹ˆ๋‹ค.

 

Span์€ ๊ทธ ์—ฌ์ •์˜ ๊ฐ ๋‹จ๊ณ„์ž…๋‹ˆ๋‹ค.

 

API ํ˜ธ์ถœ ํ•˜๋‚˜, ํ•จ์ˆ˜ ์‹คํ–‰ ํ•˜๋‚˜๊ฐ€ ๊ฐ๊ฐ ํ•˜๋‚˜์˜ Span์ž…๋‹ˆ๋‹ค.

 

Span์€ ์‹œ์ž‘(startSpan)๊ณผ ์ข…๋ฃŒ(span.end())๊ฐ€ ๋ช…์‹œ์ ์œผ๋กœ ์žˆ์Šต๋‹ˆ๋‹ค.

 

๊ทธ ์‚ฌ์ด์— attributes๋ฅผ ๋ถ™์ด๊ฑฐ๋‚˜, ์—๋Ÿฌ๋ฅผ ๊ธฐ๋กํ•˜๊ฑฐ๋‚˜, ์ƒํƒœ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

 

์ข…๋ฃŒ๋˜์ง€ ์•Š์€ Span์€ OTLP ์—”๋“œํฌ์ธํŠธ๋กœ ์ „์†ก๋˜์ง€ ์•Š์œผ๋‹ˆ, end()๋ฅผ ๋น ๋œจ๋ฆฌ์ง€ ์•Š๋Š” ๊ฒŒ ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.

 

๐Ÿ”Ž Resource: ๋‚ด ์•ฑ์ด ๋ˆ„๊ตฌ์ธ์ง€ ์•Œ๋ ค์ฃผ๋Š” ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ

Resource๋Š” "์ด Trace๋ฅผ ๋ณด๋‚ธ ๊ฒŒ ์–ด๋–ค ์„œ๋น„์Šค๋ƒ"๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์ž…๋‹ˆ๋‹ค.

 

service.name, service.version, os.name ๊ฐ™์€ ์†์„ฑ๋“ค๋กœ ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค.

 

๋ชจ๋‹ˆํ„ฐ๋ง ํˆด์—์„œ ํ•„ํ„ฐ๋งํ•  ๋•Œ ์ด Resource ์ •๋ณด๊ฐ€ ๊ธฐ์ค€์ด ๋ฉ๋‹ˆ๋‹ค.

 

์ œ๋Œ€๋กœ ์„ค์ •ํ•ด๋‘์ง€ ์•Š์œผ๋ฉด ์–ด๋А ์„œ๋น„์Šค์—์„œ ์˜จ Trace์ธ์ง€ ๊ตฌ๋ถ„์ด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.

 

๐Ÿ”Ž SpanProcessor์™€ Exporter์˜ ๊ด€๊ณ„

Exporter๋Š” Span ๋ฐ์ดํ„ฐ๋ฅผ ์–ด๋””๋กœ ๋ณด๋‚ผ์ง€๋ฅผ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค.

 

์ฝ˜์†”์— ์ถœ๋ ฅํ•˜๊ฑฐ๋‚˜(ConsoleSpanExporter), HTTP๋กœ OTLP ์—”๋“œํฌ์ธํŠธ์— ์ „์†กํ•ฉ๋‹ˆ๋‹ค(OTLPTraceExporter).

 

SpanProcessor๋Š” Exporter๋ฅผ ๊ฐ์‹ธ๋Š” ๋ž˜ํผ์ž…๋‹ˆ๋‹ค.

 

BatchSpanProcessor๋Š” Span์„ ๋ชจ์•„์„œ ์ผ๊ด„ ์ „์†กํ•˜๊ณ , SimpleSpanProcessor๋Š” ์ฆ‰์‹œ ์ „์†กํ•ฉ๋‹ˆ๋‹ค.

 

๐Ÿ“Œ Web SDK๋กœ RN SDK ๋งŒ๋“ค๊ธฐ

๐Ÿ”Ž ํŒจํ‚ค์ง€ ๊ตฌ์„ฑ

pnpm add @opentelemetry/sdk-trace-web@2.0.0 \
         @opentelemetry/resources@2.0.1 \
         @opentelemetry/exporter-trace-otlp-http@0.200.0 \
         @opentelemetry/instrumentation@0.203.0 \
         @opentelemetry/instrumentation-fetch@0.200.0 \
         @opentelemetry/otlp-exporter-base@0.200.0 \
         @opentelemetry/api

sdk-trace-web์„ RN์—์„œ ์“ด๋‹ค๋Š” ๊ฒŒ ํ•ต์‹ฌ์ž…๋‹ˆ๋‹ค.

 

RN์˜ JS ๋Ÿฐํƒ€์ž„์€ Hermes ๋˜๋Š” JSC์ธ๋ฐ, ๋ธŒ๋ผ์šฐ์ €์ฒ˜๋Ÿผ fetch๋‚˜ XMLHttpRequest๊ฐ€ ๊ธ€๋กœ๋ฒŒ๋กœ ์žˆ์–ด์„œ Web SDK๊ฐ€ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

 

๋‹จ, RN์˜ fetch๋Š” ์ง„์งœ ๋ธŒ๋ผ์šฐ์ € fetch๊ฐ€ ์•„๋‹ˆ๋ผ whatwg-fetch polyfill → XHR → Native(NSURLSession/OkHttp) ๋กœ ์ด์–ด์ง€๋Š” ๊ตฌ์กฐ๋ผ, Web SDK ์ž…์žฅ์—์„œ๋Š” ๋ธŒ๋ผ์šฐ์ € ํ™˜๊ฒฝ์ฒ˜๋Ÿผ ๋ณด์ด์ง€๋งŒ ์‹ค์ œ ๋„คํŠธ์›Œํฌ๋Š” ๊ฐ OS์˜ ๋„คํŠธ์›Œํฌ ์Šคํƒ์ด ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

 

์›น์ด๋ž‘ ์™„์ „ํžˆ ๋™์ผํ•˜์ง€ ์•Š์•„ ์™„๋ฒฝํ•˜์ง„ ์•Š์ง€๋งŒ, ๋Œ€๋ถ€๋ถ„์˜ ๊ธฐ๋Šฅ์€ ๋ณ„๋„ ์„ค์ •์—†์ด ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.

 

๐Ÿ”Ž OptlConfig ์ธํ„ฐํŽ˜์ด์Šค

์„ค์ • ์ธํ„ฐํŽ˜์ด์Šค๋ถ€ํ„ฐ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

interface OptlConfig {
  url: string; // OTLP ์—”๋“œํฌ์ธํŠธ URL
  accessToken: string; // ์ธ์ฆ ํ† ํฐ
  serviceName: string; // ๋ชจ๋‹ˆํ„ฐ๋ง ํˆด์—์„œ ์‹๋ณ„ํ•  ์„œ๋น„์Šค๋ช…
  serviceVersion: string;
}

 

๐Ÿ”Ž SpanProcessor ์ด์ค‘ ์„ค์ • (Console + OTLP)

๊ฐœ๋ฐœ ์ค‘์—๋Š” ์ฝ˜์†”์—์„œ ๋ฐ”๋กœ ํ™•์ธํ•˜๊ณ , ํ”„๋กœ๋•์…˜์—์„œ๋Š” OTLP ์—”๋“œํฌ์ธํŠธ๋กœ ์ „์†กํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

spanProcessors ๋ฐฐ์—ด์— ๋‘˜ ๋‹ค ๋“ฑ๋กํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

const logSpanProcessor = new BatchSpanProcessor(new ConsoleSpanExporter());
const otlpSpanProcessor = new BatchSpanProcessor(
  new OTLPTraceExporter({
    url: config.url,
    headers: {
      "Content-Type": "application/json",
      Authorization: config.accessToken,
    },
  }),
);

tracerProvider = new WebTracerProvider({
  resource,
  spanProcessors: [logSpanProcessor, otlpSpanProcessor],
});

๋ฐฐ์—ด์— ์—ฌ๋Ÿฌ SpanProcessor๋ฅผ ๋„ฃ์œผ๋ฉด Span์ด ๋๋‚  ๋•Œ ๋“ฑ๋ก๋œ ์ˆœ์„œ๋Œ€๋กœ ๋ชจ๋‘ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

 

๐Ÿ”Ž FetchInstrumentation์œผ๋กœ ์ž๋™ ์ถ”์ 

fetch ํ˜ธ์ถœ์„ ์ผ์ผ์ด Span์œผ๋กœ ๊ฐ์‹ธ๋Š” ๊ฑด ํ˜„์‹ค์ ์ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

 

FetchInstrumentation์„ ๋“ฑ๋กํ•˜๋ฉด ๋ชจ๋“  fetch ํ˜ธ์ถœ์ด ์ž๋™์œผ๋กœ Span์œผ๋กœ ์ถ”์ ๋ฉ๋‹ˆ๋‹ค.

registerInstrumentations({
  instrumentations: [
    new FetchInstrumentation({
      propagateTraceHeaderCorsUrls: /.*/, // ๋ชจ๋“  ๋„๋ฉ”์ธ์— tracecontext ํ—ค๋” ์ „ํŒŒ
      clearTimingResources: false,
      applyCustomAttributesOnSpan: (span, request, result) => {
        if (!(result as Response).ok) {
          span.setStatus({
            code: api.SpanStatusCode.ERROR,
            message: `${(result as Response).status} ${(result as Response).url}`,
          });
        }
      },
    }),
  ],
});

applyCustomAttributesOnSpan ์ฝœ๋ฐฑ์—์„œ HTTP ์—๋Ÿฌ๋ฅผ ์žก์•„ Span์— ์—๋Ÿฌ ์ƒํƒœ๋ฅผ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค.

 

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด 4xx/5xx ์‘๋‹ต๋„ Trace์— ๋‚จ์Šต๋‹ˆ๋‹ค.

 

๐Ÿ”Ž Resource์— OS ์ •๋ณด ๋‹ด๊ธฐ

import { Platform } from "react-native";
import { defaultResource, resourceFromAttributes } from "@opentelemetry/resources";

const resource = defaultResource().merge(
  resourceFromAttributes({
    "service.name": config.serviceName,
    "service.version": config.serviceVersion,
    "deployment.environment.name": process.env.NODE_ENV,
    "os.name": Platform.OS, // 'ios' | 'android'
    "os.version": Platform.Version, // iOS: '17.0', Android: 33
  }),
);

Platform.OS์™€ Platform.Version์œผ๋กœ iOS/Android๋ฅผ ๊ตฌ๋ถ„ํ•ฉ๋‹ˆ๋‹ค.

 

๋ชจ๋‹ˆํ„ฐ๋ง ํˆด์—์„œ ํ”Œ๋žซํผ๋ณ„๋กœ ํ•„ํ„ฐ๋งํ•  ๋•Œ ์ด ๊ฐ’์ด ๊ธฐ์ค€์ด ๋ฉ๋‹ˆ๋‹ค.

 

defaultResource()๋Š” SDK๊ฐ€ ์ž๋™์œผ๋กœ ์ฑ„์›Œ์ฃผ๋Š” ๊ธฐ๋ณธ ์†์„ฑ๋“ค(SDK ๋ฒ„์ „ ๋“ฑ)์ด ๋‹ด๊ฒจ ์žˆ์Šต๋‹ˆ๋‹ค.

 

.merge()๋กœ ์ปค์Šคํ…€ ์†์„ฑ์„ ๋ง๋ถ™์ด๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.

 

๐Ÿ“Œ ๊ฐ€์žฅ ๊ณ ํ†ต์Šค๋Ÿฌ์šด ๋ถ€๋ถ„: metro.config.js

SDK ์ฝ”๋“œ ์ž์ฒด๋Š” ์–ด๋ ต์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์ง„์งœ ๋ฌธ์ œ๋Š” ๋ฒˆ๋“ค๋ง์ด์—ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์™œ ๋ฒˆ๋“ค๋ง์ด ๊นจ์ง€๋Š”๊ฐ€

metro๋ฅผ ๊ธฐ๋ณธ ์„ค์ •์œผ๋กœ ๋‘๊ณ  ์•ฑ์„ ์‹คํ–‰ํ•˜๋ฉด ๋‘ ๊ฐ€์ง€ ์—๋Ÿฌ๊ฐ€ ์ˆœ์„œ๋Œ€๋กœ ๋‚ฉ๋‹ˆ๋‹ค.

 

์ฒซ ๋ฒˆ์งธ

Unable to resolve module '@opentelemetry/otlp-exporter-base/browser-http'
  None of these files exist:
    * node_modules/@opentelemetry/otlp-exporter-base/browser-http(.ios.js|.native.js|.js|...)

@opentelemetry/otlp-exporter-base ํŒจํ‚ค์ง€๋Š” package.json์˜ exports ํ•„๋“œ๋กœ ์„œ๋ธŒํŒจ์Šค๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

 

Metro๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ exports ํ•„๋“œ๋ฅผ ๋ฌด์‹œํ•˜๊ณ  ํŒŒ์ผ ์‹œ์Šคํ…œ์—์„œ ์ง์ ‘ ๊ฒฝ๋กœ๋ฅผ ์ฐพ์œผ๋ ค ํ•ฉ๋‹ˆ๋‹ค. ๋‹น์—ฐํžˆ ์‹คํŒจํ•˜์ฃ .

 

๋‘ ๋ฒˆ์งธ

Unable to resolve module './semconvStability' from
  'node_modules/@opentelemetry/instrumentation/build/src/...'

@opentelemetry/instrumentation ํŒจํ‚ค์ง€ ๋‚ด๋ถ€์—์„œ ์ƒ๋Œ€ ๊ฒฝ๋กœ๋กœ ./semconvStability๋ฅผ importํ•ฉ๋‹ˆ๋‹ค.

 

Metro๊ฐ€ ์ด ์ƒ๋Œ€ ๊ฒฝ๋กœ๋ฅผ ํŒจํ‚ค์ง€ ๋‚ด๋ถ€ ์ปจํ…์ŠคํŠธ์—์„œ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ํ•ด์„ํ•˜์ง€ ๋ชปํ•˜๋Š” ๋ฒ„๊ทธ์ž…๋‹ˆ๋‹ค.

 

๐Ÿ”Ž ํ•ด๊ฒฐ์ฑ…: unstable_enablePackageExports + alias + resolveRequest

// metro.config.js
const { getDefaultConfig, mergeConfig } = require("@react-native/metro-config");

const config = {
  resolver: {
    // ๋ฌธ์ œ 2 ํ•ด๊ฒฐ: ์ƒ๋Œ€ ๊ฒฝ๋กœ๋ฅผ ์ ˆ๋Œ€ ๊ฒฝ๋กœ๋กœ ๊ฐ•์ œ ๋งคํ•‘
    alias: {
      "./semconvStability": require.resolve("@opentelemetry/instrumentation/build/src/semconvStability.js"),
    },

    // ๋ฌธ์ œ 1 ํ•ด๊ฒฐ: browser-http ๋ชจ๋“ˆ๋งŒ exports ํ•„๋“œ๋ฅผ ํ™œ์„ฑํ™”ํ•ด ํ•ด์„
    resolveRequest: (context, moduleImport, platform) => {
      if (moduleImport === "@opentelemetry/otlp-exporter-base/browser-http") {
        return context.resolveRequest({ ...context, unstable_enablePackageExports: true }, moduleImport, platform);
      }
      return context.resolveRequest(context, moduleImport, platform);
    },
  },
};

module.exports = mergeConfig(getDefaultConfig(__dirname), config);

ํ•ต์‹ฌ ํฌ์ธํŠธ๋Š” ๋‘ ๊ฐ€์ง€์ž…๋‹ˆ๋‹ค.

 

์ฒซ์งธ, unstable_enablePackageExports๋ฅผ ์ „์—ญ์œผ๋กœ ์ผœ์ง€ ์•Š๊ณ  ํ•ด๋‹น ๋ชจ๋“ˆ์—๋งŒ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.

 

์ „์—ญ์œผ๋กœ ์ผœ๋ฉด ๋‹ค๋ฅธ RN ๋‚ด๋ถ€ ํŒจํ‚ค์ง€๋“ค์ด ๊นจ์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

๋‘˜์งธ, alias๋Š” Metro์˜ resolver ๋ ˆ๋ฒจ์—์„œ ๋™์ž‘ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํŒจํ‚ค์ง€ ๋‚ด๋ถ€์˜ ์ƒ๋Œ€ ๊ฒฝ๋กœ๋„ ์žก์•„๋‚ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

๐Ÿ“Œ ์‹ค์ œ ์‚ฌ์šฉ ์˜ˆ์‹œ

๐Ÿ”Ž initialize() ํ˜ธ์ถœ ์‹œ์ 

App.tsx ์ตœ์ƒ๋‹จ์—์„œ ๋ชจ๋“ˆ ์Šค์ฝ”ํ”„๋กœ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.

 

React lifecycle ๋ฐ–์—์„œ ์‹คํ–‰๋˜์–ด์•ผ FetchInstrumentation์ด ์•ฑ ์‹œ์ž‘๋ถ€ํ„ฐ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

// App.tsx
import optl from "./optl";

optl.initialize({
  url: "YOUR_OTLP_ENDPOINT",
  accessToken: "YOUR_ACCESS_TOKEN",
  serviceName: "my-rn-app",
  serviceVersion: "1.0.0",
});

 

๐Ÿ”Ž startSpan / endSpan / recordError ์‚ฌ์šฉ๋ฒ•

// ์ˆ˜๋™ Span: ํŠน์ • ๋กœ์ง์˜ ์‹คํ–‰ ์‹œ๊ฐ„ ์ถ”์ 
const handlePayment = async () => {
  const span = optl.startSpan("payment.process", {
    "payment.method": "card",
    "cart.total": 15000,
  });

  try {
    await processPayment();
    optl.endSpan(span); // OK ์ƒํƒœ๋กœ ์ข…๋ฃŒ
  } catch (error) {
    optl.endSpan(span, error); // ERROR ์ƒํƒœ๋กœ ์ข…๋ฃŒ + ์˜ˆ์™ธ ๊ธฐ๋ก
  }
};

// ์—๋Ÿฌ ๋‹จ๋… ๊ธฐ๋ก
const handleError = (error: Error) => {
  optl.recordError(error);
};

 

๐Ÿ”Ž FetchInstrumentation ์ž๋™ ์ถ”์ 

๋ณ„๋„ ์ฝ”๋“œ ์—†์ด fetch๋ฅผ ๊ทธ๋ƒฅ ์“ฐ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

// ์ด ํ˜ธ์ถœ์ด ์ž๋™์œผ๋กœ Span์œผ๋กœ ์ถ”์ ๋ฉ๋‹ˆ๋‹ค
const response = await fetch("https://api.example.com/users");

๋ชจ๋‹ˆํ„ฐ๋ง ํˆด์—์„œ๋Š” ์ด๋ ‡๊ฒŒ ์ฐํž™๋‹ˆ๋‹ค.

GET https://api.example.com/users
  duration: 234ms
  http.status_code: 200
  http.method: GET
  service.name: my-rn-app
  os.name: ios

404๋‚˜ 500 ์‘๋‹ต์€ applyCustomAttributesOnSpan ์ฝœ๋ฐฑ์—์„œ ERROR ์ƒํƒœ๋กœ ๊ธฐ๋ก๋˜๋‹ˆ ์—๋Ÿฌ Trace๋„ ๋”ฐ๋กœ ์ถ”์ ๋ฉ๋‹ˆ๋‹ค.

 

๐Ÿ”Ž ์ „์—ญ JS ์—๋Ÿฌ ์ž๋™ ํฌ์ฐฉ

HTTP ์š”์ฒญ ์™ธ์— try/catch๋กœ ์žกํžˆ์ง€ ์•Š์€ ์ „์—ญ JS ์˜ˆ์™ธ๋„ Trace๋กœ ๋‚จ๊ฒจ์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

RN์€ ErrorUtils.setGlobalHandler()๋กœ ์ด๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

const previousHandler = ErrorUtils.getGlobalHandler();
ErrorUtils.setGlobalHandler((error, isFatal) => {
  this.recordError(error, { 'exception.is_fatal': String(isFatal) });
  previousHandler?.(error, isFatal);
});

isFatal ํ”Œ๋ž˜๊ทธ๋ฅผ attribute๋กœ ํ•จ๊ป˜ ๊ธฐ๋กํ•˜๋ฉด "์•ฑ์ด ๊ฐ•์ œ ์ข…๋ฃŒ๋œ ์—๋Ÿฌ"์™€ "๊ทธ๋ƒฅ ๋„˜์–ด๊ฐ„ ์—๋Ÿฌ"๋ฅผ ๊ตฌ๋ถ„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

๊ธฐ์กด ํ•ธ๋“ค๋Ÿฌ(previousHandler)๋ฅผ ์ฒด์ด๋‹ํ•˜๋Š” ๊ฒƒ๋„ ์ค‘์š”ํ•œ๋ฐ, Sentry ๊ฐ™์€ ๋‹ค๋ฅธ ์—๋Ÿฌ ๋ฆฌํฌํŒ… ๋„๊ตฌ๊ฐ€ ์ด๋ฏธ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ๋“ฑ๋กํ•ด๋’€์„ ์ˆ˜ ์žˆ๊ฑฐ๋“ ์š”.

 

๋ฎ์–ด์“ฐ๋ฉด ๋‘˜ ์ค‘ ํ•˜๋‚˜๊ฐ€ ์—๋Ÿฌ๋ฅผ ๋ชป ๋ฐ›์Šต๋‹ˆ๋‹ค.

 

๐Ÿ”Ž ErrorBoundary๋กœ React ๋ Œ๋” ์—๋Ÿฌ ํฌ์ฐฉ

๋ฌธ์ œ๋Š” ErrorUtils๊ฐ€ ์ „๋ถ€๊ฐ€ ์•„๋‹ˆ๋ผ๋Š” ์ ์ž…๋‹ˆ๋‹ค.

 

React ์ปดํฌ๋„ŒํŠธ ๋ Œ๋” ์ค‘ ๋ฐœ์ƒํ•œ ์—๋Ÿฌ๋Š” React๊ฐ€ ๋‚ด๋ถ€์ ์œผ๋กœ ๋จผ์ € ์ฒ˜๋ฆฌํ•˜๊ธฐ ๋•Œ๋ฌธ์— ErrorUtils ํ•ธ๋“ค๋Ÿฌ๊นŒ์ง€ ๋„๋‹ฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

 

๋” ๋‚˜์œ ๊ฑด, ErrorBoundary ์—†์ด ๋ Œ๋” ์—๋Ÿฌ๊ฐ€ ๋‚˜๋ฉด ์ปดํฌ๋„ŒํŠธ ํŠธ๋ฆฌ ์ „์ฒด๊ฐ€ ์–ธ๋งˆ์šดํŠธ๋˜๊ณ  JS ์Šค๋ ˆ๋“œ๊ฐ€ ์ค‘๋‹จ๋ฉ๋‹ˆ๋‹ค.

 

์ดํ›„ ErrorUtils๊ฐ€ ํ˜ธ์ถœ๋˜๋”๋ผ๋„ ์ด๋ฏธ ๋ณต๊ตฌ ๋ถˆ๊ฐ€ ์ƒํƒœ์ธ ๊ฒฝ์šฐ๊ฐ€ ์ƒ๊ธฐ์ฃ .

 

componentDidCatch()์—์„œ info.componentStack์„ attribute๋กœ ๊ธฐ๋กํ•˜๋ฉด ์–ด๋А ์ปดํฌ๋„ŒํŠธ์—์„œ ์—๋Ÿฌ๊ฐ€ ํ„ฐ์กŒ๋Š”์ง€ Trace๋กœ ์ถ”์ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

import React from 'react';
import {Text, View} from 'react-native';
import optl from './optl';

interface Props {
  children: React.ReactNode;
}

interface State {
  hasError: boolean;
}

class ErrorBoundary extends React.Component<Props, State> {
  state: State = {hasError: false};

  componentDidCatch(error: Error, info: React.ErrorInfo) {
    optl.recordError(error, {
      'exception.type': 'ReactRenderError',
      'react.component_stack': info.componentStack ?? '',
    });
  }

  static getDerivedStateFromError(): State {
    return {hasError: true};
  }

  render() {
    if (this.state.hasError) {
      return (
        <View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
          <Text>Something went wrong.</Text>
        </View>
      );
    }
    return this.props.children;
  }
}

export default ErrorBoundary;

JS ๋Ÿฐํƒ€์ž„ ์—๋Ÿฌ๋Š” ErrorUtils๊ฐ€, React ๋ Œ๋” ์—๋Ÿฌ๋Š” ErrorBoundary๊ฐ€ ๊ฐ๊ฐ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค.

 

๋‘ ๋ ˆ์ด์–ด๋ฅผ ๋ชจ๋‘ ์ปค๋ฒ„ํ•ด์•ผ ์•ฑ ์—๋Ÿฌ๋ฅผ ๋น ์ง์—†์ด OTEL๋กœ ๋ณด๋‚ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


 

์ฒ˜์Œ์—” "Web SDK๋ฅผ RN์—์„œ ๊ทธ๋ƒฅ ๋Œ๋ ค๋„ ๋˜๋‚˜?" ๋ฐ˜์‹ ๋ฐ˜์˜ํ•˜๋ฉฐ ์‹œ์ž‘ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

๊ทธ๋Ÿฐ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง ํˆด ํ™”๋ฉด์— Trace๊ฐ€ ์ฐํžˆ๋Š” ์ˆœ๊ฐ„, ์ฒ˜์Œ์œผ๋กœ ์•ฑ์ด "๋ณด์ด๋Š”" ๋А๋‚Œ์„ ๋ฐ›์•˜์Šต๋‹ˆ๋‹ค.

 

API ์‘๋‹ต ์‹œ๊ฐ„์ด ์ˆซ์ž๋กœ ๋‚˜์˜ค๊ณ , ์–ด๋–ค ํ™”๋ฉด์—์„œ ์–ด๋–ค ์š”์ฒญ์ด ์–ผ๋งˆ๋‚˜ ๊ฑธ๋ ธ๋Š”์ง€๊ฐ€ ํ•œ๋ˆˆ์— ๋“ค์–ด์˜ค๋Š” ๊ฒฝํ—˜์ด์—ˆ์Šต๋‹ˆ๋‹ค.

 

๋„์ž…ํ•œ์ง€ ๋ช‡ ๊ฐœ์›”์ด ์ง€๋‚œ ์ง€๊ธˆ, ์ด์Šˆ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ์–ด๋А ๊ตฌ๊ฐ„์—์„œ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ฒผ๋Š”์ง€ ๋น ๋ฅด๊ฒŒ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ๊ณ , ์—๋Ÿฌ ํŒจํ„ด์„ ๋ฏธ๋ฆฌ ๊ฐ์ง€ํ•ด ์„ ์ œ์ ์œผ๋กœ ๋Œ€์‘ํ•  ์ˆ˜ ์žˆ๋Š” ํ™˜๊ฒฝ์ด ๊ฐ–์ถฐ์กŒ์Šต๋‹ˆ๋‹ค.

 

ํˆด์—์„œ ์ง์ ‘ ์ œ๊ณตํ•˜๋Š” SDK๋ฅผ ์‚ฌ์šฉํ•ด๋„ ์ข‹์ง€๋งŒ ์ง์ ‘ ๊ตฌํ˜„ํ•œ๋‹ค๋ฉด ๋น„์šฉ์ ์ธ ๋ฉด์—์„œ ๋ฉ”๋ฆฌํŠธ๊ฐ€ ์žˆ์œผ๋‹ˆ ์กฐ์‹ฌํžˆ ์ถ”์ฒœ ๋“œ๋ ค๋ด…๋‹ˆ๋‹ค.

๋ฐ˜์‘ํ˜•

๋Œ“๊ธ€


์˜คํ”ˆ ์ฑ„ํŒ