[React] useState는 비동기이다. 동기적으로 사용하는 방법은?

박기영·2022년 8월 11일
12

React

목록 보기
6/32
post-custom-banner

문제 상황

프로젝트를 진행하다보면 로그인, 회원가입 등을 구현하기 위해서 input 태그를 사용하는 일이 꽤 자주 있다. 혹은, 수량 선택 기능을 만들기 위해서 증감 버튼을 만들 때도 있다.
보통 이런 기능을 구현할 때, 우리가 가장 먼저 하는 것은 useState로 state를 생성하는 것이다. 그런데...내가 의도한대로 안될 때가 있다.
비밀번호 확인을 위해서 두 state를 비교하려고 하는데, 값은 같은데 계속 틀리다고 한다거나,
수량을 한번에 여러개 증가시키도록 코딩했는데 하나만 증가한다거나.
왜 이런 문제가 발생하는걸까?

useState는 동기적으로 작동하지 않아!

제목에도 나와있지만 useState는 비동기적으로 작동한다.
필자가 겪었던 상황을 예시로 하여, 살펴보도록 하자!
필자는 비밀번호와 비밀번호 확인 input을 각각 만들어서 거기에 입력된 값들을 비교하고자 했다.
당시 코드는 아래와 같았다.

function JoinPage() {
  // password
  const [pswd, setPswd] = useState<string>("");
  const [pswdMessage, setPswdMessage] = useState<string>("");
  const [isPswd, setIsPswd] = useState<boolean>(false);

  // password check
  const [checkPswd, setCheckPswd] = useState<string>("");
  const [checkPswdMessage, setCheckPswdMessage] = useState<string>("");
  const [isCheckPswd, setIsCheckPswd] = useState<boolean>(false);

  // password input onChange
  const onChangePswd = (e: React.ChangeEvent<HTMLInputElement>) => {
    setPswd(e.target.value);
    
	const pswdRegEx = /^(?=.*[a-zA-Z])(?=.*[!@#$%^])(?=.*[0-9]).{8,25}$/;

    if (!pswdRegEx.test(pswd)) {
      setPswdMessage(
        "숫자+영문+특수문자(!,@,#,$,%,^) 조합으로 입력해주세요."
      );
      setIsPswd(false);
    } else {
      setPswdMessage("비밀번호가 정상적으로 입력되었습니다.");
      setIsPswd(true);
    }
  };

  // password check input onChange
  const onChangeCheckPswd = (e: React.ChangeEvent<HTMLInputElement>) => {
    setCheckPswd(e.target.value);
    
	if (pswd !== checkPswd) {
      setCheckPswdMessage("맞게 입력했는지 다시 확인해주세요.");
      setIsCheckPswd(false);
    } else {
      setCheckPswdMessage("비밀번호 확인이 완료되었습니다.");
      setIsCheckPswd(true);
    }
};

  return (
    <Layout>
      <div className="flex flex-col items-center my-16">
        <h1 className="mb-4 md:text-xl">Join</h1>

        <form
          className="flex flex-col items-center mb-12"
          onSubmit={submitHandler}
        >
          <div className="flex flex-col items-center">
            <input
              type="password"
              className="border-2 border-black mb-2 h-12 pl-2 w-[300px] md:w-[500px] md:text-xl focus:outline-none"
              placeholder="비밀번호를 입력해주세요(8 - 25 자리)"
              onChange={onChangePswd}
              value={pswd}
              required
            />

            <span
              className={
                isPswd
                  ? "text-green-400 font-bold mb-2"
                  : "text-red-700 font-bold mb-2 px-4 text-center"
              }
            >
              {pswdMessage}
            </span>
          </div>

          <div className="flex flex-col items-center">
            <input
              type="password"
              className="border-2 border-black mb-2 h-12 pl-2 w-[300px] md:w-[500px] md:text-xl focus:outline-none"
              placeholder="비밀번호를 다시 한번 입력해주세요"
              onChange={onChangeCheckPswd}
              value={checkPswd}
              required
            />

            <span
              className={
                isCheckPswd
                  ? "text-green-400 font-bold mb-2"
                  : "text-red-700 font-bold mb-2"
              }
            >
              {checkPswdMessage}
            </span>
          </div>
        </form>
      </div>
    </Layout>
  );
}

export default JoinPage;

useState가 비동기 처리를 한다는 사실을 모른채로 코드를 보면 아무런 의심없이 잘 작동할 것이라고 생각할 가능성이 높다.(필자가 그랬다..)
필자는 비밀번호와 비밀번호 확인 input에 같은 값을 입력했고, 비밀번호 비교가 성공했음을 알리는 글이 보일 것이라고 예상했다.

그러나...10번을 넘게 시도해봐도 비밀번호와 비밀번호 확인의 state가 일치하지 않는다고 하는 것이다...!

참고 이미지

어이가 없어서 콘솔을 찍어보았고, 결과는 다음과 같았다.

참고 이미지

??????????????? 뭐야 이게?
비밀번호 확인 부분에서 onChange가 완벽하게 작동하지 않는 것을 확인했다.
즉, 위 코드의 이 부분에서 state의 업데이트가 예상과 다르게 작동한 것이다.

const onChangeCheckPswd = (e: React.ChangeEvent<HTMLInputElement>) => {
    setCheckPswd(e.target.value);
    
    // 여기서 state가 다르게 찍히고 있는 것이다.
  	// 그래서 계속 else 부분이 실행되고있다!
	if (pswd !== checkPswd) {
      setCheckPswdMessage("맞게 입력했는지 다시 확인해주세요.");
      setIsCheckPswd(false);
    } else {
      setCheckPswdMessage("비밀번호 확인이 완료되었습니다.");
      setIsCheckPswd(true);
    }
};

onChange 발생 후 해당 함수 실행시, setState를 한 뒤에 바로 조건문을 실행하도록 했지만, 업데이트 값이 들어가기 전에 조건문이 실행되버린 것이다.
만약, useState가 동기적으로 작동하는 것이었다면 의도한대로 코드가 잘 돌아갔을 것이다.

useState는 왜 비동기적이지?

왜 useState(정확히는 setState)는 비동기적으로 작동하는 것일까?

페이지를 구성하는데는 수많은 state가 존재한다. 만약 하나하나의 state 변화에 리랜더링을 발생시킨다면 성능 저하가 발생할 것이다.
이를 해결하기 위해서, React는 setState가 연속 호출되면 배치(batch) 처리를 통해 한번에 랜더링하게 하는 것이다.
많은 setState를 연속으로 사용해도 배치 처리로 인해 랜더링은 한번만 되는 것이다.

배치(batch)란 React가 여러개의 state 업데이트를 하나의 리랜더링으로 묶는 것을 의미한다.
React는 16ms 동안 변경된 상태 값들을 하나로 묶는다. (16ms 단위로 배치를 진행한다.)

이러한 이유로 useState가 비동기 처리 되는 것이다.
하나를 바꾸면 바로 하나가 바뀐 상태가 업데이트되는 방식이 아니었던 것이다.

useState를 동기적으로 사용하려면?

필자도 많은 서칭을 통해 다양한 방법이 있다는 것을 알았지만, 가장 쉽고, 이해하기 쉬운 방식은 useEffect를 사용하는 것이었다.
useEffect의 dependency에 원하는 state를 넣어놓고 그 것이 변경되면 useEffect 내부의 함수들이 실행되도록 하는 원리를 활용할 것이다.
가장 위에서 봤던 코드를 고쳐보자.

function JoinPage() {
  // password
  const [pswd, setPswd] = useState<string>("");
  const [pswdMessage, setPswdMessage] = useState<string>("");
  const [isPswd, setIsPswd] = useState<boolean>(false);

  // password check
  const [checkPswd, setCheckPswd] = useState<string>("");
  const [checkPswdMessage, setCheckPswdMessage] = useState<string>("");
  const [isCheckPswd, setIsCheckPswd] = useState<boolean>(false);

  // password input onChange
  // 이 함수는 온전히 input 태그 내부의 텍스트가 변경되는 것을 구현하기 위한 것
  const onChangePswd = (e: React.ChangeEvent<HTMLInputElement>) => {
    setPswd(e.target.value);
  };

  // password를 동기적으로 처리하기 위해 useEffect 사용
  useEffect(() => {
    // pswd의 길이가 0을 넘어갔을 때부터 실행되게 함
    // 그렇지 않으면 첫 랜더링때부터 pswdMessage가 보이게됨.
    if (pswd.length > 0) {
      // onChangePwsd에서 나왔으므로 e.target.value로 state를 업데이트할 수 없음
      // 따라서 다음과 같은 방식으로 state를 업데이트 함
      setPswd((currentValue) => currentValue);

      const pswdRegEx = /^(?=.*[a-zA-Z])(?=.*[!@#$%^])(?=.*[0-9]).{8,25}$/;

      if (!pswdRegEx.test(pswd)) {
        setPswdMessage(
          "숫자+영문+특수문자(!,@,#,$,%,^) 조합으로 입력해주세요."
        );
        setIsPswd(false);
      } else {
        setPswdMessage("비밀번호가 정상적으로 입력되었습니다.");
        setIsPswd(true);
      }
    }
  }, [pswd]);

  // password check input onChange
  // 이 함수는 온전히 input 태그 내부의 텍스트가 변경되는 것을 구현하기 위한 것
  const onChangeCheckPswd = (e: React.ChangeEvent<HTMLInputElement>) => {
    setCheckPswd(e.target.value);
  };

  // password check를 동기적으로 처리하기 위해 useEffect 사용
  useEffect(() => {
    // checkPswd의 길이가 0을 넘어갔을 때부터 실행되게 함
    // 그렇지 않으면 첫 랜더링때부터 checkPswdMessage가 보이게됨.
    if (checkPswd.length > 0) {
      // onChangePwsd에서 나왔으므로 e.target.value로 state를 업데이트할 수 없음
      // 따라서 다음과 같은 방식으로 state를 업데이트 함
      setCheckPswd((currentValue) => currentValue);

      if (pswd !== checkPswd) {
        setCheckPswdMessage("맞게 입력했는지 다시 확인해주세요.");
        setIsCheckPswd(false);
      } else {
        setCheckPswdMessage("비밀번호 확인이 완료되었습니다.");
        setIsCheckPswd(true);
      }
    }
  }, [checkPswd]);

  return (
	// .... //
}

export default JoinPage;

onChange로 실행되는 함수 외에 useEffect를 따로 만들어서 코드를 분리해줬다.
onChange로 발생하는 함수에는 setState를 넣긴했지만, 이는 input 창에서 value값이 변하는 것을 유저가 인식할 수 있게하는 용도로만 사용된다.
useEffect는 각 state를 dependency로 하여, state가 변경될 시 내부에 있는 함수들을 실행한다.

useEffect(() => {
  // ... //
},[바꾸고싶은 state]);

이 때, setState가 onChange의 e.target.value를 사용할 수 없게 되었으므로,

setState((currentValue) => currentValue);

위와 같은 방식으로 값을 가져온다.
이렇게하면 콘솔도 제대로 찍히고, 코드도 의도한대로 잘 작동하게 된다.

참고 자료

참고 자료 1
참고 자료 2
참고 자료 3

profile
나를 믿는 사람들을, 실망시키지 않도록
post-custom-banner

0개의 댓글