React | setState 비동기 문제와 useEffect

설탕·2022년 1월 13일
2

setState의 비동기적 동작

아이디와 비밀번호의 input 값에 따라 로그인 버튼을 활성화/비활성화하는 기능을 구현하고자 했다.

const Form = () => {
  const [id, setId] = useState('');
  const [password, setPassword] = useState('');
  const btnRef = useRef();
  const handleIdInput = event => {
    setId(event.target.value);
    handleBtn();
    console.log(id);
  };
  const handlePasswordInput = event => {
    setPassword(event.target.value);
    handleBtn();
    console.log(password);
  };
  const handleBtn = () => {
    btnRef.current.disabled =
      String(id).length < 5 || String(password).length < 8;
  };
  
  return (
    <form name="login" onSubmit={handleSubmit}>
      <input
        ref={idRef}
        className="id"
        type="text"
        placeholder="전화번호, 사용자 이름 또는 이메일"
        onInput={handleIdInput}
      />
      <input
        className="password"
        type="password"
        placeholder="비밀번호"
        onInput={handlePasswordInput}
      />
      <button ref={btnRef} type="submit" disabled>
        로그인
      </button>
    </form>
  );
};

예상대로라면 아이디와 비밀번호의 input 값이 바뀔 때마다 handleIdInput, handlePasswordInput 함수가 실행되고, 입력된 input 값이 콘솔에 찍혀야 했다. 그러나 handleBtn 함수가 예상대로 작동하지 않았고, input 값이 입력할 때보다 한 박자 늦게 콘솔에 출력되는 현상을 발견했다. input에 123을 입력하면 12가 콘솔에 찍히고, 1234를 입력하면 123이 콘솔에 찍히는 것이었다.

원인은 setState가 비동기적(Asynchronous)으로 동작하기 때문이었다.

setState는 이벤트 핸들러 내에서 비동기적으로 동작한다.
하나의 이벤트 핸들러 내에서 setState가 여러 번 호출된다면, 이벤트가 끝날 시점에 state를 일괄적으로 업데이트하고 렌더링한다. 즉, 리액트는 setState 호출 즉시 state를 변경하고 리렌더링하는 것이 아니라, 여러 차례 setState가 있으면 여러 state의 변경을 통합해서 한꺼번에 리렌더링한다.

이는 리액트에서 효율성 향상을 위해 설정한 것이다. 사이트 규모가 커지고 state가 많아질 경우 setState를 실행할 때마다 state를 모두 변경하고 DOM을 모두 리렌더링한다면 사이트에 부담이 커질 것이다.

하지만 나는 setState를 호출할 때가 아니라 state가 변경될 때 버튼을 활성화/비활성화하는 기능이 필요했다.

useEffect

useEffect를 통해 이 문제를 해결할 수 있었다.

useEffect에 전달된 함수는 기본적으로 화면에 모든 렌더링이 완료된 후에 수행된다. 두 번째 인자로 [state]를 전달하면, 특정 state가 변경되었을 때만 실행되게 할 수 있다.

const Form = () => {
  const [id, setId] = useState('');
  const [password, setPassword] = useState('');
  const btnRef = useRef();
  const handleIdInput = event => {
    setId(event.target.value);
  };
  const handlePasswordInput = event => {
    setPassword(event.target.value);
  };
  useEffect(() => {
    btnRef.current.disabled =
      String(id).length < 5 || String(password).length < 8;
  }, [id, password]);
  
  return (
    ...
  );
};

setId, setPassword가 호출되는 즉시 id와 password state를 변경해주지 않는다고 해도 상관 없었다. [id, password]를 두 번째 인자로 전달했으므로 어차피 state가 변경될 때 버튼 활성화/비활성화가 이루어지기 때문이다.

useEffect 훅에 대한 React 공식문서의 설명

  • useEffect가 하는 일은 무엇일까요?
    useEffect Hook을 이용하여 우리는 React에게 컴포넌트가 렌더링 이후에 어떤 일을 수행해야하는 지를 말합니다. React는 우리가 넘긴 함수를 기억했다가(이 함수를 ‘effect’라고 부릅니다) DOM 업데이트를 수행한 이후에 불러낼 것입니다.

  • useEffect를 컴포넌트 안에서 불러내는 이유는 무엇일까요?
    useEffect를 컴포넌트 내부에 둠으로써 effect를 통해 count state 변수(또는 그 어떤 prop에도)에 접근할 수 있게 됩니다. 함수 범위 안에 존재하기 때문에 특별한 API 없이도 값을 얻을 수 있는 것입니다.

  • useEffect는 렌더링 이후에 매번 수행되는 걸까요?
    네, 기본적으로 첫번째 렌더링과 이후의 모든 업데이트에서 수행됩니다.(나중에 effect를 필요에 맞게 수정하는 방법에 대해 다룰 것입니다.) 마운팅과 업데이트라는 방식으로 생각하는 대신 effect를 렌더링 이후에 발생하는 것으로 생각하는 것이 더 쉬울 것입니다. React는 effect가 수행되는 시점에 이미 DOM이 업데이트되었음을 보장합니다.

참고: React의 setState가 잘못된 값을 주는 이유

profile
공부 기록

0개의 댓글