setState가 내 마음처럼 동작하지 않는 이유(feat.비동기)

삼콩·2022년 6월 14일
9

React

목록 보기
3/3
post-thumbnail

전에 공부한 자식 컴포넌트가 부모 컴포넌트에게 데이터를 전달하는 코드 속에서 궁금한 점이 생겼다. 사용자가 input값을 입력해 onchange 이벤트가 발생하면, 해당 값이 id:입력값상태로 v객체에 추가가 되어야하는데, 1을 누르고 2를 누르고 3을 입력해야 그때 1이 객체에 추가가 됐다. 1을 누르면 1이 바로 들어가게 하고싶은데, 어째서 두박자가 느리게 행동하는거야!

😩setState는 비동기로 동작한다!

useState에서 상태를 변경해주는 함수가 비동기로 동작해 일어난 문제였다.

일단 위 코드는 너무 복잡하니 setState의 비동기 동작을 이해하기 위해 코딩애플님의 state 변경함수 사용할 때 주의점 : async 글을 통해 쉽게 이해해보자.

쉽게 setState 비동기 동작 이해하기!

기능 요구사항

버튼을 누를 때마다
(1) count라는 state를 +1 해야합니다. (버튼누른 횟수 기록용)
(2) age라는 state도 +1 해야합니다.
(3) 근데 count 가 3 이상이면 더 이상 age라는 state를 1 더하지 말도록 코드를 짜십시오.

코앱님의 요구사항에 따라 버튼을 누르면 버튼을 누르는 횟수인 count가 올라가고, age의 값이 올라가게 코드를 짰다.

 export default function App() {
  let [count, setCount] = useState(0);
  let [age, setAge] = useState(20);
  return (
    <Contain>
      <div>안녕하십니까 전 {age}</div>
      <button
        onClick={() => {
          setCount(count + 1);
          // 근데 count 가 3 이상이면 더 이상 age라는 state를 1 더하지 말도록 코드를 짜십시오.
          if (count < 3) {
            setAge(age + 1);
          }
        }}
      >
        누르면한살먹기
      </button>
    </Contain>
  );
}

조건문으로 count가 3미만의 횟수로 누르면 age가 올라가게해놨는데 23까지 올라간다. 이건 내가 코드를 잘못짠것이 아니다. 이것이 바로 setCount가 비동기적으로 동작하기 때문에 벌어진 일이다! setCount 함수는 뒤로 밀려 count의 값이 1만큼 오르기전에, if문이 실행돼 count가 아직 2일때 setAge가 실행되는 것이다. 바로 이럴때 useEffect를 사용해준다!

useEffect
useEffect(함수, [감시대상(여러개 넣어줄 수 있음)])
감시대상을 적어주면 감시대상이 변할때마다 안의 함수를 실행해준다. 따라서 감시대상이 변할 때, 원하는 동작을 실행시켜줄 수 있게 되는 것!

export default function App() {
  let [count, setCount] = useState(0);
  let [age, setAge] = useState(20);
  useEffect(() => {if (count < 3) {setAge(age + 1);}}, [count]);return (
    <Contain>
      <div>안녕하십니까 전 {age}</div>
      <button
        onClick={() => {setCount(count + 1);}}
      >
        누르면한살먹기
      </button>
    </Contain>
  );
}

✨ 이렇게 변경해주면 useEffect가 count를 감시하고 있다가, 변경되면 안의 함수를 실행해주기 때문에, count가 변하고나서 if문이 수행되어 올바르게 작동해....지않는다. 20이 초기값인데 1이 더해진 21로 시작한다.

진짜 열반네?! 그 이유는 useEffect에 의존값(두번째 파라미터로 받는 배열)이 있을 시, 컴포넌트가 마운트됐을 때 (처음 나타났을 때) 실행이 한번 된 다음에, 의존값이 변할때마다 실행되기 때문이다. 따라서 조건문에 count != 0이 조건을 추가해 해당 코드를 다시 한번 고쳐주면

  let [count, setCount] = useState(0);
  let [age, setAge] = useState(20);
  useEffect(() => {
    if (✨count != 0&& count < 3) {
      setAge(age + 1);
    }
  }, [count]);

드디어 20으로 시작하고 3번 이상 누르면 age가 올라가지 않는다! 겨우겨우 만들었네 휴


어렵게 setState 비동기 동작 이해하기!

이제 아까 말한 코드에서 자식 컴포넌트의 setInput함수를 살펴보자.

const Child = (props) => { 
   const [input, setInput] = useState(); //🍎

   const handleonChange = (event) => {
       console.log(event)// 이벤트 객체 찍힘

       const {name, value} = event.target //🍊
       console.log(name, value) // id 1 

       setInput({[name]: value}) // 비동기로 동작함.
       console.log(input)// 먼저 찍힘 따라서 undefined
       props.onChange(input) //🍌 
   }
   return (
       <div>
           <p>{props.one}</p>
           <p>{props.two}</p>
           <p>{props.three}</p>
           <input type="text" name="id" onChange={handleonChange} />
       </div>
   )
}

우리가 1을 입력하게 되면, input의 onChange 이벤트가 실행된다. 그러면 handleonChange 함수가 콜백함수로 호출되게 되는데, 이 함수에서 🍊부분을 보자. event.target에서 가져온 name과 value를 비구조화 할당을 통해 할당해준다음, setInput함수를 통해 [name]:value 로 input값을 바꿔주고있다.

❗️하지만 여기서 주의할 부분은 이 setInput함수가 비동기식으로 동작하기 때문에, 뒤로 밀리고 console.log(input)이 먼저 찍힌다!

위로 올라가 🍎 부분을 보면 useState(빈값)이기 때문에 input의 초기값은 undefined이다. 따라서 console.log(v)를 하게되면 undefined가 주어진다. 그리고 이어서 다음줄인 🍌 부분을 실행하는데, input이 undefined이기 때문에 value로 전달된 input은 아래코드의 🥝부분에서 ...value를 해도 ...undefined와 같기 때문에 객체에 추가되지 않는 것이다.

const Parent = () => {
   const [ v, setV ] = useState({
       a: 101,
       b: 'hello',
       c: 'world'
   })

   return (
       <div>
       <Child one={v.a} two={v.b} three={v.c} onChange={value => {
           console.log('Parent')
           console.log(v)
           console.log(value)
           return setV({...v, ...value})//🥝
       }} />
       {v.id}
       </div>
   )
}

💢setState야, 짱나게 왜 비동기로 동작하니?

setState: 왜냐묜 내가 상태를 변경해줄 때마다 컴포넌트가 재랜더링되기 때문에, 컴포넌트의 상태값이 많을 때 이 상태값들이 바뀔 때마다 재랜더링된다면 성능상 이슈가 생길 수 있기 때문이야!

그렇다고 한다. 따라서, 어떤 상태값이 바뀐 후 동작해야하는 코드가 있다면, useEffect를 사용해 해당 값을 감시하다가 상태가 변하면 동작하도록 해주어야한다는 것을 기억하자!

❓의문점

1. 콜백함수로 순차적으로 진행되게 하면 되지 않나?

이는 클래스 컴포넌트에서는 동작하지만, 이 방법은 함수형 컴포넌트에서 상태관리를 가능하게 해주는 useState에서는 콜백함수를 넣어줄 수 가 없다.

2. async await은 사용못하나?

promise를 반환하지 않기 때문에 동작이 안된다.

=> 따라서 useEffect가 등장한 것!


모든 의문점은 이 글에서 풀렸다 : Provide callback to useState hook like setState

//전체코드 
import React, { useState } from 'react';

const Parent = () => {
    const [ v, setV ] = useState({
        a: 101,
        b: 'hello',
        c: 'world'
    })

    return (
        <div>
        <Child one={v.a} two={v.b} three={v.c} onChange={value => {
            console.log('Parent')
            console.log(v)
            console.log(value)
            return setV({...v, ...value})
        }} />
        {v.id}
        </div>
    )
}

const Child = (props) => { 
    const [input, setInput] = useState();

    const handleonChange = (event) => {
        console.log(event)

        const {name, value} = event.target
        console.log(name, value)

        setInput({[name]: value})
        console.log(input)
        props.onChange(input)
    }
    return (
        <div>
            <p>{props.one}</p>
            <p>{props.two}</p>
            <p>{props.three}</p>
            <input type="text" name="id" onChange={handleonChange} />
        </div>
    )
}


function App() {
    return (
        <div>
            <Parent/>
        </div>
    );
}

export default App; 
profile
프론트엔드 세계의 모략을 꾸미는 김삼콩입니다

0개의 댓글