Redux
@sophonz/react-native-redux 미들웨어를 훅을 쓰거나 쓰지 않고 Redux Toolkit 스토어에 추가하는 방법과, 디스패치된 액션마다 정확히 무엇이 기록되는지 설명합니다.
@sophonz/react-native-redux는 디스패치되는 모든 액션의 스팬을 기록하는 Redux 미들웨어입니다. 액션 타입, 리듀서와 나머지 미들웨어 체인에 걸린 시간, 예외 발생 여부, 페이로드 자체가 아닌 페이로드 크기가 담깁니다. 패키지는 순수 JavaScript이며 tracer provider를 통해 기록합니다.
설치
npm install @sophonz/react-native-redux@0.1.0 @sophonz/react-native-tracer-provider@0.1.0@reduxjs/toolkit이 peer dependency입니다. 미들웨어는 Toolkit의 Middleware 타입으로 정의되어 있고 configureStore로 추가합니다.
훅을 쓰는 경우
미들웨어는 tracer provider가 생긴 뒤, 즉 SDK가 시작된 뒤에야 만들 수 있습니다. middleware가 준비되면 스토어를 만드세요.
import {useEffect, useState} from "react";
import {Provider} from "react-redux";
import {configureStore, type EnhancedStore} from "@reduxjs/toolkit";
import {useSophonz} from "@sophonz/react-native";
import {useSophonzNativeTracerProvider} from "@sophonz/react-native-tracer-provider";
import {useSophonzMiddleware} from "@sophonz/react-native-redux";
import {rootReducer} from "./store/rootReducer";
const SOPHONZ_CONFIG = {trackUnhandledRejections: true};
export default function App() {
const {isStarted} = useSophonz(SOPHONZ_CONFIG);
const {tracerProvider} = useSophonzNativeTracerProvider(undefined, isStarted);
const {middleware} = useSophonzMiddleware(tracerProvider);
const [store, setStore] = useState<EnhancedStore>();
useEffect(() => {
if (middleware && !store) {
setStore(
configureStore({
reducer: rootReducer,
middleware: getDefaultMiddleware => getDefaultMiddleware().concat(middleware),
}),
);
}
}, [middleware, store]);
if (!store) {
return null; // 또는 스플래시 화면
}
return <Provider store={store}>{/* ... */}</Provider>;
}위처럼 Sophonz 미들웨어를 체인의 마지막에 두세요. 그래야 스팬이 리듀서만 측정하고 앞선 미들웨어는 포함하지 않습니다. 뒤에 놓인 미들웨어(예를 들어 thunk 하나를 여러 액션으로 바꾸는 미들웨어)는 바깥 액션의 시간에 포함됩니다.
훅을 쓰지 않는 경우
스토어를 React 밖에서 만든다면 SDK를 먼저 시작하고 provider와 미들웨어를 직접 만듭니다.
import {configureStore} from "@reduxjs/toolkit";
import {initialize} from "@sophonz/react-native";
import {SophonzNativeTracerProvider} from "@sophonz/react-native-tracer-provider";
import {createSophonzMiddleware} from "@sophonz/react-native-redux";
import {rootReducer} from "./rootReducer";
export const createStore = async () => {
await initialize({sdkConfig: {trackUnhandledRejections: true}});
const tracerProvider = new SophonzNativeTracerProvider();
return configureStore({
reducer: rootReducer,
middleware: getDefaultMiddleware =>
getDefaultMiddleware().concat(createSophonzMiddleware(tracerProvider)),
});
};new SophonzNativeTracerProvider()는 SDK 실행 여부를 확인하지 않습니다. initialize가 resolve되기 전에 만들면 네이티브 쪽이 기록할 수 없는 스팬이 생깁니다.
기록 내용
type이 있는 액션마다 스팬 하나입니다. 그 밖의 값은 건드리지 않고 통과시킵니다.
| 필드 | 값 |
|---|---|
| 스팬 이름 | action |
spz.type | sys.rn_action |
name | cart/itemAdded 같은 액션 타입 |
payload_size | type을 뺀 액션을 JSON으로 만들어 gzip 압축한 바이트 수 |
outcome | 시작 시 incomplete. next(action)이 반환되면 success, 예외가 나면 fail |
action.state | 액션이 끝났을 때의 AppState.currentState: active, background, inactive |
| 소요 시간 | 디스패치부터 next(action)이 반환될 때까지 |
리듀서나 뒤쪽 미들웨어가 예외를 던지면 스팬은 outcome: fail로 끝나고 에러는 그대로 다시 던져집니다. 스팬 상태는 ERROR로 설정되지 않습니다.
액션 페이로드는 기기를 떠나지 않습니다. 압축 크기만 기록되며, 평소보다 훨씬 많은 데이터를 싣게 된 액션을 찾기에는 그것으로 충분합니다.
비동기 작업
스팬은 동기 디스패치 구간만 측정합니다. createAsyncThunk라면 요청 전체를 덮는 스팬 하나가 아니라 pending, fulfilled 또는 rejected의 짧은 스팬 세 개가 됩니다. 요청 자체는 네트워크 스팬으로 기록됩니다. 작업 전체를 재려면 thunk 본문을 커스텀 스팬으로 감싸세요.
양
디스패치마다 네이티브 호출이 세 번(스팬 시작, 결과 설정, 종료) 일어납니다. 키 입력이나 애니메이션 프레임마다 디스패치하는 앱은 그만큼 스팬을 쏟아냅니다. 그런 앱이라면 액션이 의미 있는 스토어에만 적용하거나, 중요한 흐름에만 커스텀 스팬을 쓰세요.
API
| API | 설명 |
|---|---|
useSophonzMiddleware(tracerProvider?) | {middleware}를 돌려줍니다. tracerProvider가 생기기 전에는 undefined이고, 이후에는 렌더링이 반복돼도 같은 미들웨어입니다 |
createSophonzMiddleware(tracerProvider) | 미들웨어를 바로 만듭니다 |
tracer 이름은 @sophonz/react-native-redux, 버전은 0.1.0입니다.