Santos의 개발블로그

react useState에 대한 공통적인 실수 본문

Language & Framework & Library/React

react useState에 대한 공통적인 실수

Santos 2024. 3. 17. 09:12

* 이 글은 Commons Mistakes with React useState hook and How to Prevent them. 번역하였습니다.

 

Commons Mistakes with React useState hook and How to Prevent them.

A list of tricky errors I’ve seen on various react projects without developers realizing that could destroy their whole app!

medium.com


저는 다양한 개발자들이 전체 Application을 망치고 있는 것을 인지하지 못하는 react project에서 까다로운 에러들을 보았습니다. 


React의 useState는 React Application의 핵심적인 부분입니다. 그러나 많은 개발자들이 이 기능이 어떻게 생겨먹었는지 알지 못한채로 사용합니다. 

 

useStates는 string, boolean 또는 number와도 같은 원시 타입에서는 사용하기 쉬운 반면에 object, map 그리고 array와도 같은 참조 타입에서는 약간 까다롭니다. 

 

useStates 사용 실수로 인해 나온 공통적인 질문은 다음과 같습니다. 

 

1. 왜 React의 state값을 변경하면 UI에서 최근 state 값을 볼수 없나요?

2. 저는 React state값을 변경했는데 왜 바뀐 부분을 볼 수 없나요? 

 

어쩔때는 성공(값이 바뀔수도)할수도 아니면 실패할지도 모르는 랜덤한 상황이 항상 발생하기 때문에 확실하게 아는 것이 중요합니다. 

 

Not using previous value when based on the previous state 

그럼 이전 상태에서 React state를 어떻게 계산하나요? 아마도 공통적인 에러이고, 여전히 저는 이러한 코드들을 많이 봅니다. 

const App = () => {
  const [counter, setCounter] = useState(0);
  
  return (
    <button onClick={() => setCounter(counter + 1)}>
      Increment me {counter}
    </button>
  );
};

아마도 이 코드는 랜덤하게 동작할 수도 있지만 굉장히 좋지 않은 코드입니다. React official 문서에서 봤음에도 불구하고, 이 코드는 동작합니다. 하지만 React는 Dom (또는 React Native에서 동등한 것)과의 상호 작용을 줄이기 위해 상태 업데이트를 대기열에 넣고 계산하기 때문에 안전하지는 않습니다. 

import React, { Component } from 'react';
import { render } from 'react-dom';
import Hello from './Hello';
import './style.css';

class App extends Component {
  constructor() {
    super();
    this.state = {
      functionalScore: 0,
      score: 0
    };
  }

  conventionalIncreaseScoreBy3 = () => {
    this.setState({score : this.state.score + 1});
    this.setState({score : this.state.score + 1});
    this.setState({score : this.state.score + 1});
  }

  functionalIncreaseScoreBy3 = () => {
    this.setState(prevState => ({ functionalScore: prevState.functionalScore + 1 }));
    this.setState(prevState => ({ functionalScore: prevState.functionalScore + 1 }));
    this.setState(prevState => ({ functionalScore: prevState.functionalScore + 1 }));
  }



  render() {
    return (
      <div>        
        <input type="button" onClick={this.conventionalIncreaseScoreBy3} value="CONVENTINAL SET STATE" />
        <input type="button" onClick={this.functionalIncreaseScoreBy3} value="FUNCTIONAL SET STATE" />

        <div>
          CONVENTIONAL SCORE: {this.state.score}
        </div>
        <div>
          FUNCTIONAL SCORE: {this.state.functionalScore}
        </div>
      </div>
    );
  }
}

render(<App />, document.getElementById('root'));

만약에 문제가 발생하면 이전 값과는 아무런 관련이 없고, 잘못된 계산 값으로 끝날 수 있습니다. 이전의 state 값을 가지고 액션을 취해야 하는 한 이전 state값이 주어지고 새로운 state값을 return하는 함수를 사용해야 합니다. 이러한 방법에서 React는 새로운 state값을 받아올 수 있는 함수를 체이닝합니다. 

const App = () => {
  const [counter, setCounter] = useState(0);
  
  return (
    <button onClick={() => 
      setCounter(prevCounter => prevCounter + 1)}
    >
      Increment me {counter}
    </button>
  );
};

자바 스크립트에서는 클로저를 사용하여 값을 전달할 수 있지만 가능하다면 종속성 주입 코드를 만드는 것이 좋습니다.

 

클로저 (특히 Public API에서 오는 변수들을 숨길 수 있는 factory functions들) 사용하는 것은 유용하지만 좋은 패턴은 아닙니다. 

Updating state reference instead of re-assign it 

useState가 다양한 곳에서 사용됨에도 불구하고, 가장 많은 실수는 어떻게 useState가 동작하는지를 이해하는 것입니다. 

 

useState는 state값이 바뀔 때 서로간의 얕은 비교를 통해서 업무를 수행합니다. 이 말은 즉슨 object 나 array를 변경할 때 참조 값 자체를 수정하는 것이 필요합니다. 

 

아래의 코드는 App UI가 변경되지 않는 부분을 보여줍니다. 

const App = () => {
  const [state, setState] = useState({ a: null });
  return (
    <button 
      onClick={() =>
        setState((previousState) => {
          previousState.a = "newValue";
          return previousState;
        })
      }
    >
      ChangeMe {state.a}
    </button>
  );
};

보시는 바와 같이 previousState 같은 참조 값을 바라보고 있고, 중첩된 property만을 수정합니다. 해당 알고리즘은 object 나 array는 참조 값이므로 값의 변화를 찾아낼 수 없습니다. 

 

이러한 상황을 해결할 수 있는 한가지 방법은 새로운 참조 값을 만들고 이를 사용하는 것입니다.  

const App = () => {
  const [state, setState] = useState({ a: null });
return (
    <button 
      onClick={() =>
        setState((previousState) => {
          return Object.assign({}, previousState, {a: "newValue"})
          // Or return {...previousState, a: "newValue"}
        })
      }
    >
      ChangeMe {state.a}
    </button>
  );
};

새로운 참조를 사용하므로, 모든 값은 수정됩니다. 이것은 React의 불변성을 유지 하기 위한 보통의 패턴입니다. 

불변성을 유지하기 쉽게 만든 아래의 라이브러리를 살펴보세요. 

 

rtfeldman/seamless-immutable

Immutable data structures for JavaScript which are backwards-compatible with normal JS Arrays and Objects. - rtfeldman/seamless-immutable

github.com

불변성을 유지하기 위해서는 배열에서 유명한 새로운 배열을 리턴하는 filter나 map과 같은 함수형 프로그래밍 함수들을 사용하셔도 됩니다.

Wanting to store a function as a state to change it

많은 시도를 해보았지만, React는 declaratively하게 생각을 하도록 디자인 되어 있는 반면 보통 우리들은 imperatively 하게 생각을 합니다. 무엇보다도 함수를 사용할 때 이전에 말씀드렸던 React가 어디서 previous state를 제공하여 업데이트를 할 수 있는지 대한 해결책을 완벽하게 이해하셔야 합니다. 

 

함수를 변경한 state값으로써 저장 하고 싶다면, 함수를 리턴하는 함수를 제공하는 것이 필요합니다. 정말 좋지 않은 코드지만 한번 try 해보죠. 

const App = () => {
  const [callback, setCallback] = useState(() => () => "Hello world");
  return (
    <button
      onClick={() => {
        // Execute the callback code
        setCallback(() => () => "Hello WORLD"+ Math.random())
      }}
    >
      Click me {callback()}
    </button>
  );
};

개발적인 관점에서 매우 친숙하지 않은 코드여도, 이 코드는 동작을 합니다. 이것은 useState를 이용하여 바꾸고, on-demand로 값을 계산합니다. 

 

이러한 코드보다는 useCallback 또는 useMemo를 사용하여 state를 기반으로 한 callback함수를 만드는 것이 훨씬 좋습니다. 

const App = () => {
  const [clickId, setClickId] = React.useState(0);
  const callback = useCallback(() => "Hello WORLD"+ Math.random(), [clickId])
  return (
    <button
      onClick={() => {
        // Make the callback stale
        setClickId(prev => prev + 1)
      }}
    >
    Click me {callback()}
    </button>
  );
};

아래의 페이지는 위에 적혀있는 코드와 동일한 또 다른 방법입니다. 전적으로 cache가 없는 카운터를 사용하는 declarative한 프로그래밍을 해야합니다.

 

Coding Spark

Un écosystème pour les développeurs désirant créer des applications web et mobiles de qualité en utilisant JavaScript et d'autres langages modernes.

codingspark.io

긴 글 읽어주셔서 감사합니다.


< 참고자료 >

 

[사이트] #Midum

medium.com/javascript-in-plain-english/commons-mistakes-with-react-usestate-hook-and-how-to-prevent-them-43c811ca7451

 

Commons Mistakes with React useState hook and How to Prevent them.

A list of tricky errors I’ve seen on various react projects without developers realizing that could destroy their whole app!

medium.com

 

<React> Commons mistakes with React useState hook and how to prevent them end

 
Comments