[리액트] 당신의 useEffect가 mobx와 작동하지 않는 이유

Date:     Updated:

카테고리:

태그:


🚀Introduction

왜 작동이 되지 않을까?

useEffect(() => {
console.log("useEffect작동!");
}, [NumberStore.num]);

나는 분명히 숫자를 바꿨는데 왜 useEffect가 작동되지 않을까? 결론부터 말하자면 메모리 위치가 바뀌지 않았기 때문이다. 리액트를 쓰는 사람들이라면 상태가 바뀔 때, 불변성 원칙에 따라 값이 변경되는건 당연하리라 생각을 할 것이다. 간단하지 않았던 여정에서 알게된 것들을 공유하고자 한다.


🚀너는 왜 안해줘?

먼저, 읽기 귀찮겠지만 코드를 먼저 보여주고 설명을 이어나가는게 좋을 것 같다. 버튼을 누르면 숫자가 올라가는 아주간단한 컴포넌트이다

//numberStore.js (mobxStore)
const NumberStore = observable({ 
    num : {x: 7},
});

export default NumberStore;
//App.js
const App = () => {
  useEffect(() => {
    console.log("useEffect작동!");
  }, [NumberStore.num]); //NumberStore.num.x 는 변경을 인식 함
  
  return 
  <>
    <p> 숫자: {JSON.stringify(numberStore.num)}</p>
    <p> 숫자: {numberStore.num.x}</p>
    <Button/> //버튼을 누르면 화면에 숫자를 올려주는 버튼 
  </>;  
};

export default observer(App);

위의 코드를 실행시키고 버튼을 누르면 숫자가 아주 잘 올라간다. 하지만 useEffect는 실행될 기미가 보이지 않는다. 컴포넌트는 리렌더링이 되었지만 useEffect는 실행되지 않았다. 왜 그럴까? 분명히 UI는 바뀌었다. 하지만 왜 useEffect는 변경사항을 감지하지 못할까?


🤔왜 안해줘?

다음글 에서는 mobx의 핵심작동원리에 대해 분석할 것이다. 위 글을 읽었다는 가정하에 설명을 계속 나아가겠다. mobx에 대한 이해를 하고싶지 않거나, 이미 알고 계신분들은 아래 내용의 요지만 파악하고 넘어가면 될 것 같다.

  1. mobx의 값 변경은 프록시를 이용한다. 레퍼런스(메모리 위치)를 바꾸지 않고 값을 변경해준다.
  2. useState로 선언된 값들은 값을 변경해주면 무조건 레퍼런스가 바뀐다.
  3. useEffect의 dependency array에 들어가 있는 값들은 레퍼런스가 바뀌어야만 실행된다.
  4. mobx의 값 변경은 레퍼런스가 바뀔때가 있고, 바뀌지 않을때가 있다.
  5. 내가 값을 바꾼 방식 에 따라 useEffect가 동작할때도 있고, 하지 않을때도 있다.
  6. 레퍼런스를 잘 바꿔주면 잘 작동한다
  7. 레퍼런스를 바꿔주기 어려울때는 reaction을 사용하면 잘 작동한다
  8. reaction의 의존성은 스토어 속성에 따라 다르게 적어주어야 한다

아래 두개의 차이를 보자

//1번
const NumberStore = observable({
  num : 7,
});

값변경해주는함수: (num) => {
  NumberStore.num = NumberStore.num + num;
};
//2번
const NumberStore = observable({
  num : {x: 7},
});

값변경해주는함수: (num) => {
  NumberStore.num = {x: NumberStore.num.x + num};
};

1번은 number를 바로 바꿔주지만, 2번은 객체의 값을 조작한다. 당연하게도 2번은 proxy를 통해 객체의 값을 조작해 주기 때문에 레퍼런스가 변하지 않는다. 그렇다. 마치 다음과 같이 동작하는 것이다.

//정말 이상한 코드
const [, 값설정] = useState(0)
handleClick = () => ++

설사 useState를 넣더라도 위처럼 setValue를 사용하지 않고 않고 값을 바꾸어주면 당연히 레퍼런스의 값이 바뀌지 않으므로 렌더링 되지 않는다. 따라서, mobx의 불변성을 깨주면서 값을 변경하거나 강제로 렌더링을 시키는 방법을 사용함으로써 mobx의 상태변화를 useEffect에서 인지하도록 만들어 줄 수 있다.


🤯그래서 어떻게 해야해?

  1. mobx의 불변성을 깨주면서 값을 변경해주기
    this.num.x = newNum //이러면 useEffect 안먹음
    this.num = {x: newNum} //이러면 useEffect 먹음
    
  2. 강제로 렌더링을 시켜주기
useEffect(() => {
  const disposer = reaction(
    () => NumberStore.num,
    (newValue, prevValue) => {
      console.log("내가 원하던 사이드이펙트 발동!");
      console.log(`값은 ${prevValue} 에서 ${newValue}가 됨`);
    }
  );

  // 컴포넌트가 언마운트될 때 해지되도록 cleanup!!
  return () => {
    disposer();
  };
}, []);

위와같은 두가지 방법이 있다. 첫번째로 새로운 레퍼런스를 만들어주는것이 best이지만 상황에 따라 어려울수도 있다. 혹은 내가 변경하기 어려운, 살짝 툭 치면 모든것이 망가져버리는 코드들을 건드리기 겁니지만 빨리 개발을 해야하는 상황(ㅎㅎ…)에서는 2번째 방법을 사용할 수 있다. mobx공식문서에서도 제한적인 상황에서만 사용하라고 가이드하고 있다.

그렇다면 다소 생소할수 있는 두번째 방법을 보도록 하겠다. 아쉽게도 위의 두번째 방법은 작동하지 않는다.

reaction()의 첫번째 인자로 넣어준 부분인 () => NumberStore.num 이 부분이 문제이다. 이 부분은 JSON.stringify(NumberStore.num)이렇게 넣어주어야 한다. 위의 스토어 정의를 보면 다음과 같이 정의되었기 때문이다.

const NumberStore = observable({
  num : {x: 7},
});

따라서 이와같은 경우 아래처럼 코드를 작성해야 한다

//작동 안하는 useEffect
useEffect(() => {
  const disposer = reaction(
    () => NumberStore.num,  //첫번째인자: 감시하는 값
    () => console.log("내가 원하던 사이드이펙트 발동!") //두번째인자: 값이 변경되었을때 실행되는 함수
  );
}, []);

//작동 잘하는 useEffect
useEffect(() => {
  const disposer = reaction(
    () => JSON.stringify(NumberStore.num),  //첫번째인자: 감시하는 값
    () => console.log("내가 원하던 사이드이펙트 발동!") //두번째인자: 값이 변경되었을때 실행되는 함수
  );
}, []);

왜 이렇게 작성을 해야할까? 그 이유에 대해서는 다음글(useSyncExternalStore과 함께 알아보는 mobx의 작동원리) 에서 다루도록 하겠다


🕵🏽‍♂️메모리 위치를 확인해 보자

더욱 근사한 설명을 덧붙이기 위해, 할당되는 메모리 위치를 찍어보고싶었으나 가볍게 사전조사해본 결과 자바스크립트에서는 (C언어처럼)쉽사리 메모리를 확인하기 어렵다는 결론이 나왔다.

레퍼런스1-인프런 질문답변

레퍼런스2-개발 유튜브


🚀결론

우리는 직접 눈으로 mobx의 값변경이 일어날 때 메모리 위치가 어떻게 변하는지에 대해서 살펴보지는 못했다. 하지만 다양한 경험적 추론(에 리액트 공식문서를 곁들임)을 통해 깊은복사를 통해 상태를 변경해 주면 레퍼런스가 바뀌고 이를 감지하여 리액트가 작동함을 알고있다. mobx에서는 프록시 객체를 통해 변경사항을 구독자 컴포넌트님들께 알려주는 observer패턴을 사용하는 것을 알고있다. 하지만 useEffect의 dependency array에서는 작동을 잘 하지 않는 문제를 겪을 수 있었으며, 이를 해결하기 위한 두가지 방법을 배웠다. props 혹은, state로 값을 전달해주면 이러한 문제가 생기지 않겠지만 불가피할때는 위의 두가지 방법을 사용하면 될 것 같다. 첫번째 방법인, 객체복사를 통한다면 useEffect는 항상 원하는 순간에 잘 작동을 할 것이다. 하지만 상황이 여의치 않을때는 mobx에서 제공하는 reaction을 이용해서 상황을 해결할 수 있다. 다음글에서 자세한 작동원리를 파악하면 이처럼 mobx를 사용하며 겪을수도 있는 크고작은 문제들을 쉽게 헤쳐나갈 수 있으리라 생각한다.


맨 위로 이동하기

react 카테고리 내 다른 글 보러가기

댓글 남기기