Redux 플러그인으로 상태 변경 캡처하기
Video Tutorial
Redux로 상태를 추적하는 방법 보기
글을 읽는 것이 번거롭다면, Redux를 사용할 때 React 앱의 상태를 추적하는 방법을 보여 주는 이 동영상 튜토리얼을 따라 해 보세요
사용자 세션을 리플레이할 때 더 많은 정보가 필요하다면, 애플리케이션 상태를 살펴보는 것이 큰 도움이 될 수 있습니다.
Redux의 경우 OpenReplay는 스토어의 내부 동작에 연동할 수 있는 플러그인을 제공합니다. 이 플러그인을 사용하면 녹화된 세션 전반에 걸쳐 Redux 스토어의 상태(state)와 디스패치된 액션을 확인할 수 있습니다.
설정을 마치면 다음 스크린샷과 같이 스토어의 변경 사항을 확인할 수 있습니다.

Next.js 프로젝트에 Redux 설정하기
Section titled Next.js 프로젝트에 Redux 설정하기이 튜토리얼에서는 Next.js로 만든 일반적인 이커머스 사이트인 이 저장소(redux-store 브랜치)를 사용합니다.
이 프로젝트에서는 강조 표시된 상품 목록을 외부 API에서 가져온 새로운 상품 목록으로 교체합니다.
이를 위해 Axios로 상품을 요청하는 함수를 추가하고, 이 함수를 Redux 액션 안에서 호출하겠습니다.
참고: 이 애플리케이션은 복잡한 Next.js 애플리케이션이라서 일반적인 To-Do 앱의 표준 구조와는 다를 수 있습니다. 그래도 이 튜토리얼을 따라가면 변경 사항을 충분히 이해할 수 있습니다.
기억해 두세요: 저장소를 클론하여 코드를 직접 살펴볼 수도 있습니다.
먼저 다음 명령어로 주요 의존성을 모두 설치합니다.
npm i next-redux-wrapper redux react-redux redux-thunk redux-devtools-extension
설치가 끝났다면 프로젝트의 루트 디렉터리에 store라는 폴더를 만들고 다음과 같은 구조로 구성합니다.

types.js 파일에는 앞으로 정의할 두 액션의 타입 정의를 작성합니다.
export const GET_PRODUCTS = 'GET_PRODUCTS'
export const PRODUCTS_ERROR = 'PRODUCTS_ERROR'
store.js 파일은 호출될 때 새로운 Redux 스토어를 생성하는 함수를 내보냅니다. Redux 플러그인이 반환하는 새로운 Redux 미들웨어를 추가해야 하기 때문입니다(이에 대해서는 잠시 후에 설명합니다).
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import { composeWithDevTools } from 'redux-devtools-extension'
import rootReducer from './reducers'
const initalState = {}
export default function createReduxStore(extraMiddleware = []) {
const middleware = [thunk, ...extraMiddleware]
const store = createStore(
rootReducer,
initalState,
composeWithDevTools(applyMiddleware(...middleware))
)
return store
}
리듀서 파일(makeUpReducer.js)은 상품 목록 또는 문제가 발생했을 때 반환되는 오류 메시지로 상태를 업데이트합니다.
import { GET_PRODUCTS, PRODUCTS_ERROR } from '../types'
const initialState = {
makeUpProducts: [],
loading: true,
}
export default function (state = initialState, action) {
switch (action.type) {
case GET_PRODUCTS:
return {
...state,
makeUpProducts: action.payload,
loading: false,
}
case PRODUCTS_ERROR:
return {
loading: false,
error: action.payload,
}
default:
return state
}
}
마지막으로 액션 파일에는 외부 API에서 상품 목록을 가져오고 올바른 액션과 페이로드를 디스패치하는 함수 하나를 정의합니다.
import { GET_PRODUCTS, PRODUCTS_ERROR } from '../types'
import axios from 'axios'
import slugify from 'slugify'
export const getMakeUpProducts = () => async (dispatch: any) => {
console.log('Getting the makeup products')
try {
let { data } = await axios.get(
'https://makeup-api.herokuapp.com/api/v1/products.json?brand=maybelline&apiKey=123fff132'
)
const products = data
let newProds = products.map((p: any) => {
return {
id: '' + p.id,
slug: slugify(p.name),
name: p.name,
description: '',
images: [{ url: p.image_link }],
variants: [],
price: {
value: +p.price,
},
options: [],
}
})
dispatch({
type: GET_PRODUCTS,
payload: newProds,
})
} catch (e) {
dispatch({
type: PRODUCTS_ERROR,
payload: e,
})
}
}
트래커 프로바이더 설정하기
Section titled 트래커 프로바이더 설정하기이 프로바이더를 사용하면 여러 플러그인을 설정할 수 있습니다. 여기서는 다음과 같이 Redux 플러그인을 사용합니다(_app.tsx 파일 내부).
//...more imports here....
import TrackerProvider from '../context/trackerProvider'
import trackerRedux from '@openreplay/tracker-redux'
// ... more code here....
export default function MyApp({ Component, pageProps }: AppProps) {
const Layout = (Component as any).Layout || Noop
useEffect(() => {
document.body.classList?.remove('loading')
}, [])
let plugins = [
{
fn: trackerRedux,
name: 'redux',
config: {},
},
]
return (
<TrackerProvider config={{ plugins }}>
<Head />
<ManagedUIContext>
<Layout pageProps={pageProps}>
<Component {...pageProps} />
</Layout>
</ManagedUIContext>
</TrackerProvider>
)
}
이 코드로 올바른 플러그인과 함께 트래커를 설정할 수 있지만, 플러그인이 동작하려면 플러그인을 호출할 때 반환되는 미들웨어에 접근해야 합니다. 즉, 플러그인이 반환하는 값을 다른 곳에서 사용할 수 있도록 보관해야 합니다. 여기서는 위의 createReduxStore 함수를 호출할 때 이 값을 사용합니다.
이를 위해 반환된 값을 상태에 보관하도록 TrackerProvider를 확장해야 합니다. 다음과 같습니다(이 파일의 전체 버전은 여기에서 확인할 수 있습니다).
import { createContext, useCallback } from 'react'
import Tracker from '@openreplay/tracker'
import { v4 as uuidV4 } from 'uuid'
import { useReducer } from 'react'
export const TrackerContext = createContext()
function defaultGetUserId() {
return uuidV4()
}
function newTracker(config) {
///code here
}
function reducer(state, action) {
switch (action.type) {
case 'init': {
if (!state.tracker) {
console.log('Instantiaing the tracker for the first time...')
let t = newTracker(state.config)
let pluginsReturnedValue = {}
if (state.config.plugins) {
state.config.plugins.forEach((p) => {
console.log('Using plugin...')
pluginsReturnedValue[p.name] = t.use(p.fn(p.config)) //keep track
})
}
return {
...state,
pluginsReturnedValue: pluginsReturnedValue, //update the state
tracker: t,
}
}
return state
}
case 'start': {
console.log('Starting tracker...')
state.tracker.start()
return state
}
}
}
export default function TrackerProvider({ children, config = {} }) {
let [state, dispatch] = useReducer(reducer, {
tracker: null,
pluginsReturnedValue: {},
config,
})
let value = {
startTracking: () => dispatch({ type: 'start' }),
initTracker: () => dispatch({ type: 'init' }),
pluginsReturnedValues: { ...state.pluginsReturnedValue }, //inject the state
}
return (
<TrackerContext.Provider value={value}>{children}</TrackerContext.Provider>
)
}
init 액션에서는 플러그인과 함께 use 메서드를 호출했을 때 반환되는 값도 함께 보관합니다. 이 딕셔너리를 state.pluginsReturnedValue 속성에 저장하고, pluginsReturnedValues 변수를 통해 모든 자식 컴포넌트에서 사용할 수 있게 합니다.
이 로직 덕분에 트래커를 초기화할 때 플러그인을 사용하고, 나중에 미들웨어에 접근하여 사용할 수 있습니다.
새 미들웨어로 Redux 스토어 만들기
Section titled 새 미들웨어로 Redux 스토어 만들기플러그인이 동작하게 되었으니 이제 Redux 스토어를 초기화해야 합니다. 이 작업은 트래커가 초기화된 후, start 메서드가 호출되기 전에 수행해야 합니다.
이를 위해 _app.tsx 파일에서 직접 사용하는 ManagedUI 컴포넌트를 선택했습니다. 이 컴포넌트는 트래커 프로바이더로 감싸져 있으므로, 공유되는 컨텍스트에 접근할 수 있습니다.
컴포넌트는 다음과 같이 생겼습니다.
export const ManagedUIContext: FC = ({ children }) => {
const { initTracker, pluginsReturnedValues } = useContext(TrackerContext)
const [store, setStore] = useState<Store>()
useEffect(() => {
initTracker()
}, [])
useEffect(() => {
if (!pluginsReturnedValues['redux']) return
let middleWares = pluginsReturnedValues['redux']
? [pluginsReturnedValues['redux']]
: []
setStore(createReduxStore(middleWares))
}, [pluginsReturnedValues])
return (
<div>
{store && (
<Provider store={store}>
<UIProvider>
<ThemeProvider>{children}</ThemeProvider>
</UIProvider>
</Provider>
)}
</div>
)
}
이 파일에서 핵심적으로 알아 둘 내용은 다음과 같습니다.
- 컨텍스트에서
initTracker함수와pluginsReturnedValues속성을 가져옵니다. - 첫 번째 함수는 컴포넌트가 마운트될 때(첫 번째
useEffect를 통해) 한 번만 호출합니다. - 그다음
pluginsReturnedValues변수에 반환된 값이 담긴 후에야 Redux 스토어를 생성합니다. 두 번째useEffect는 페이지가 로드될 때 한 번, 그리고initTracker메서드가 상태 변수를 수정할 때 한 번, 이렇게 두 번 호출됩니다. 두 번째 호출 시에는pluginsReturnedValues에 저장된 미들웨어로 스토어를 생성합니다.
트래커 시작하기
Section titled 트래커 시작하기플러그인 설정과 Redux 스토어 생성이 올바르게 끝났다면, 이제 트래커의 start 메서드를 호출하기만 하면 됩니다.
이 로직은 index.tsx 파일에 추가하며, 이 파일의 전체 소스 코드를 확인할 수 있습니다.
이 코드에서 살펴볼 관련 부분은 다음과 같습니다.
// imports and more logic goes here...
export default function Home({
products,
}: InferGetStaticPropsType<typeof getStaticProps>) {
const { startTracking } = useContext(TrackerContext)
const dispatch = useDispatch()
const makeUpProductsList = useSelector((state: any) => state.makeUpProducts)
const { makeUpProducts } = makeUpProductsList
useEffect(() => {
async function getProds() {
await startTracking()
dispatch(getMakeUpProducts() as any)
}
getProds()
}, [dispatch])
return (
<>
<Grid variant="filled">
{products.slice(0, 3).map((product: any, i: number) => (
<ProductCard
key={product.id}
product={product}
imgProps={{
width: i === 0 ? 1080 : 540,
height: i === 0 ? 1080 : 540,
priority: true,
}}
/>
))}
</Grid>
<Marquee variant="secondary">
{makeUpProducts.slice(0, 3).map((product: any, i: number) => (
<ProductCard key={product.id} product={product} variant="slim" />
))}
</Marquee>
<!-- more code here -->
</>
)
}
트래커 컨텍스트 프로바이더의 startTracking 함수와 Redux의 useSelector 훅만 사용하여 반환된 상품 목록을 가져옵니다.
useEffect 훅은 startTracking 호출을 실행하고, getMakeUpProducts 함수 호출을 디스패치하여 새로운 메이크업 상품 목록을 가져옵니다.
질문이 있으신가요?
Section titled 질문이 있으신가요?Redux 스토어를 사용하는 Next.js 기반 애플리케이션의 전체 소스 코드는 이 저장소에서 확인할 수 있습니다.
Redux 플러그인을 설정하는 중 문제가 생기면 Slack 커뮤니티로 문의해 주세요. 개발자에게 직접 질문할 수 있습니다!