redux-persist/createPersistoid: error serializing state

aborile·2023년 7월 2일
0

삽질기

목록 보기
5/9
post-thumbnail

현재 진행 중인 프로젝트에서 bigint를 다루어야 하는 케이스가 발생했다. 기존의 javascript에서 제공하는 int의 범위를 넘어가는 수를 다루어야 해서 '그럼 bigint 자료형으로 처리하면 되겠네!' 하고 간단하게 생각했는데, 문제는 이를 전역에서 관리하기 위해 redux로 추가하면서 발생했다.

개발 환경

"@reduxjs/toolkit": "^1.9.2",
"react": "18.2.0",
"react-native": "0.71.7",
"react-redux": "^8.0.5",
"redux": "^4.2.1",
"redux-persist": "^6.0.0",
...

redux-toolkit을 사용하여 redux를 구성/관리하고 있다.

Redux non-serializable items

bigint를 redux store에 넣자마자 다음과 같은 에러가 발생하였다.

A non-serializable value was detected in the state, in the path: ...

redux의 store에 담는 요소로는 plain object, array, 그리고 primitive(number, string 등)만을 넣을 것을 권장하는데, 이는 redux가 persist/rehydrate하는 과정에서 store에 담은 내용이 유실되거나 time-travel debugging이 방해받을 수 있기 때문이다.

때문에 이런 persistency나 time-travel debugging이 의도대로 동작하지 않을 수 있어도 괜찮다면 non-serializable item을 store에 넣는 것은 전적으로 사용자의 선택이라고 redux는 강력하게 경고한다. 이는 비단 bigint 뿐만 아니라 Date, Set, Map, function 등 모든 non-serializable한 타입을 넣으려 할 때 뜨는 경고인데, 대부분의 경우에서는 크게 상관없으므로 store configuration에서 getDefaultMiddleware({ serializableCheck: false })와 같이 관련 에러를 아예 disable하는 방식으로 처리하는 글이 많은 듯하다.

전체 serializableCheck를 아예 disable하고 싶지 않고 특정 케이스에서만 무시하고 싶다면, 크게 다음과 같은 방법으로 설정할 수 있다.

특정 action/path에 대해서만 무시

ignoredActions, ignoredActionPaths, ignoredPaths 옵션을 줘서 serializability check를 무시하고 싶은 특정 action이나 path를 지정할 수 있다. (만약 redux-persist를 사용한다면 이미 사용해봤을 익숙한 구성일 것이다.)

특정 type에 대해서만 무시

만약 특정 action, 또는 path가 아니라 해당하는 type에 대한 모든 serializability check를 무시하고 싶다면 isSerializable, getEntries 옵션을 사용해서 serialize하는 방법을 직접 지정할 수 있다.

  • isSerializable: 값이 serializable인지 확인하는 함수로, state에 포함된 모든 값에 재귀적으로 적용된다. 기본값은 isPlain().
  • getEntries: 각 값에서 entries를 찾는 데 사용되는 함수로, 구체적으로 제공되지 않는다면 Object.entries를 사용한다.

나는 이 방식을 사용하여 다음과 같이 구현하였는데, getEntries를 어떻게 override하는 게 좋았을지는 아직 제대로 이해 못한 것 같아서 잘 모르겠다. 일단은 문제 없이 돌아가는 듯 보이니 패스...

import { configureStore, isPlain } from "@reduxjs/toolkit";

// Augment middleware to consider bigint serializable
const isSerializable = (value: any) => {
  return typeof value === "bigint" || isPlain(value);
};
const getEntries = (value: any) => {
  return typeof value === "bigint" ? [["bigint", value.toString()] as [string, any]] : Object.entries(value);
};

const store = configureStore({
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: {
        isSerializable,
        getEntries,
        ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER, ...],
        ...
      },
    })
  ...
});

Redux-Persist

해당 문제는 이제 해결했다고 생각해서 마음 놓고 기능 개발을 하고 있었는데, 마무리하던 중 다시 한번 문제가 생겼다. 바로 redux-persist에 넣을 때, 다음과 같은 에러가 발생한 것이다.

redux-persist/createPersistoid: error serializing state [TypeError: Do not know how to serialize a BigInt]

redux-persist로 저장하는 과정에서 JSON.stringify를 사용하여 state의 serialize를 진행하는데, stringify 함수가 bigint는 처리하지 않고 에러를 발생시켜서 생긴 문제였다.

redux-persist를 configure할 때에 transforms 옵션으로 bigint를 transform하는 방법을 구현해서 넘겨주면 해결된다. JSON.stringify 함수의 두번째 인자로 replacer라고 해서 결과를 transform해서 변환하는 함수를 넘길 수 있는데, 이를 사용하여 transform을 구현하여 넘겼다.

import { createTransform, PersistConfig } from "redux-persist";

// allow you to customize the state object that gets persisted and rehydrated
const bigIntTransfrom = createTransform(
  (toDehydrate) =>
    JSON.stringify(toDehydrate, (_, value) =>
      typeof value === "bigint" ? { type: "bigint", value: value.toString() } : value,
    ),
  (toRehydrate) =>
    JSON.parse(toRehydrate, (_, value) =>
      typeof value === "object" && value?.type === "bigint" && value?.value !== undefined ? BigInt(value.value) : value,
    ),
);

const persistConfig: PersistConfig = {
  key: "my-persist-key",
  storage: AsyncStorage,
  transforms: [bigIntTransfrom],
  ...
};

References

profile
기록하고 싶은 것을 기록하는 저장소

0개의 댓글