[TIL] React, Redux, Saga 철학에 맞는 기본적인 "상태" 설계하기

햄스터아저씨·2021년 7월 21일
1

오늘 개발 중 예상치 못한 데이터가 저장되는 사건이 있었다.
A라는 값을 B로 변경한 뒤, 저장을 했지만,
A가 저장되어있는 사례였다.

무엇이 잘못되었는지 살펴보자.

0. 개발자의 의도와 오류

  1. 개발자는 redux state 값을 변경한 뒤,
    변경된 값을 즉시 저장하고 싶다.
    (아래 예시에서는 asyncSave()라는 함수를 사용했다.)

  2. 그런데 예상치 못한 값이 저장되는 문제가 발생했다.

왜 이런일이 발생한걸까?

1. 리듀서 동작

리듀서 updateDataA() 를 먼저 살펴보자.
state.dataA 의 값을 1에서 2로 바꾼다.

// Redux
const RoomSlice = createSlice({
  name: "room",
  initialState: {
    dataA: 1, //초기값 1
  },
  reducers: {
    updateDataA: (state) => {
      state.dataA = 2;
      console.log("dataA is 2"); //변경 후 결과를 출력한다.
    }
  }
})

2. 컴포넌트 함수: 값 변경 직후 값 사용

//Component
import { actions } from "@store/Room";

const Comp = () => {
  const dispatch = useDispatch();
  
  const dataA = useSelector(
    (state: Reducers) => state.dataA
  );

  return (
      <CardBox
        onPress={() => { 
          dispatch(actions.updateDataA());
    	  // updateDataA는 dataA 값을 1에서 2로 변경한다.
          // 리듀서가 dataA값을 "변경"했으니, 아래부터는 2가 분명해!
          asyncSave(dataA); //2가 저장되겠지?
        }}
      >
        <Label>{"update Data A"}</Label>
      </CardBox>
  )

3. 이때 dispatch 직후의 dataA 로 저장되는 값은?

코드를 실행하면 리듀서 마지막 라인인 "dataA is 2"가 먼저 출력된다.
즉, 리듀서는 state.dataA 값 변경을 수행한 것이다.
이후에 저장하는 asyncSave(dataA); 에는 어떤 값이 사용될까??

  • 1?
  • 2?

정답은 1 이다.
왜냐면 asyncSave(dataA)에서 dataA

  • redux state의 변경된 값을 가진 state.dataA가 아닌,
  • 현재 컴포넌트에 복사되었던, 변경 전의 dataA 이다.

Reducer가 한 state신규 값으로 변경하면,
useSelector 는 컴포넌트를 re-rendering 하여
기존 값을 날리고, 신규 값을 복사 한다.

여기서는 dispatch() 를 하여, re-rendering 이 준비됬지만,
함수가 아직 끝나지 않았으므로, 당연히 변경 전 컴포넌트가 사용하던 값 1을 그대로 사용한다.

결국 의도하지 않은 값 1 이 저장된 이후, re-rendering이 수행된다.😥

4. 그럼 어떻게 해?

저장로직이 잘못된 위치에 들어가 있다.
각 라이브러리의 철학을 다시

  • React는 상태출력하기 위한 기술이고,
  • Redux는 상태변경하기 위한 기술이고,
  • saga나 thunk등 redux 미들웨어변경 직전/직후 로직을 집어넣기 위한 기술이다.

위에서 개발자는 방금 설명한 3가지(출력 / 상태 변경 / 로직)을 구분하지 않고 컴포넌트에 모조리 넣었다가,

  1. dispatch() 직후, re-rendering 전,
    컴포넌트의 값과 state의 값이 순간적으로 맞지 않는 상태일 때, 로직이 수행되도록 위치해있다.
  2. 변경 직후 수행해야 할 로직을 출력 담당인 React 컴포넌트에서 수행하려고 한 부분이 실수다.

그렇다면 무엇이 옳은 설계인가?

dataA 의 변경된 신규 값을 참조하는 로직이 필요했다면:

  1. 변경과 관련된 작업은 리듀서인 updateDataA() 내에서 모두 수행하도록 한다.
    하나의 리듀서가 필요한 모든 변경을 하도록 하는 것이다.
    이 경우 로직에 비동기가 포함되거나, 중복된 코드가 많이 발생한다면 미들웨어를 쓰는 것이 좋다.

  2. redux-sagaredux-thunk등 미들웨어에 updateDataA()가 변경시키는 state 와 관련된 로직을 작성한다.
    asyncSave() 함수는 이름부터 비동기이므로, 쓰고싶다면 여기에 넣는 것이 올바른 설계이다.

이렇게 변경한다면 위 개발자는 앞으로 비슷한 버그를 앞으로도 영원히 만나지 않을 것이다. 😀

profile
서버도 하고 웹도 하고 시스템이나 인프라나 네트워크나 그냥 다 함.

0개의 댓글