조각조각 - setState 오류? - 비동기성

eocode·2023년 2월 18일
0

리액트 조각조각

목록 보기
2/11
post-thumbnail

setState 비동기성

오늘은 리액트를 공부하면서 초기에 헷갈렸던 점을 정리해보도록 하겠습니다. 리액트를 공부하면서 처음 접한 개념은 바로 '리액트 상태'입니다. 처음 '상태'를 공부할때 비동기나 컴포넌트 리렌더링에 대한 개념이 없었습니다. 그저 useState Hooks 사용법과 Hooks를 사용해서 상태를 변경하면 브라우저에서 바뀐 값을 알아서 리렌더링 해준다는 점만 파악하고 있었습니다.

잘못된 사용법

useState를 그저 일종의 함수처럼만 이해하고 있었습니다. 그래서 그런지 초기 잘못된 코드로 작성하고 왜 이렇게 동작하지?? 하는 의문을 가지기 일수였습니다. 아래 코드는 잘못작성한 코드의 예시입니다.

const Test = () => {
  const [test, setTest] = useState("test");
  const [test2, setTest2] = useState("test2");

  const onClickBtn = () => {
    setTest("test_click");
    setTest2(test);
  };
  
  console.log(test, test2);

  return (
    <>
      <button onClick={onClickBtn}>click</button>
    </>
  );
};

위 코드의 작성 의도는 "click" 버튼이 한번 눌리면 test 상태가 "test_click"으로 변경되고 이어서 test2가 변경된 test 상태값으로 변경되서 test2의 상태도 "test_click"로 변경되는 것이었습니다. 하지만 결과는 첫 의도와 다르게 나왔습니다.

//"click" 버튼 한번 눌렀을 때
console.log(test, test2); //test_click test

//"click" 버튼 두번 눌렀을 때
console.log(test, test2); //test_click test_click

버튼을 한번 눌렀을 때 test_click test로 의도치 않은 결과가 출력되었으며 두번 눌렀을 때 test_click test_click로 의도한 결과가 출력되었습니다. 결과를 통해 setTest와 setTest2가 함수처럼 동기적으로 동작하지 않는다는 것을 알게되었습니다. 이후 검색으로 '한 이벤트에서 리액트 state 변경은 순차적으로 처리되는게 아니라 몰아서 처리된다' 정도로만 파악하였습니다. 사실 이때 여전히 정확히 왜 이런 결과가 나오는지 이해하지 못하였습니다.

//이벤트 체인 내 동일 문제 발생 예시

const EventChain = () => {
  const [num, setNum] = useState(0);
  const [num2, setNum2] = useState(0);

  const eventA = () =>{
	setNum(99);
	eventB(num);
  };
  const eventB = (val : number) =>{
    setNum2(num)
  };
  
  console.log(num, num2);

  return (
    <>
      <button onClick={eventA}>click</button>
    </>
  );
};

그저 '이벤트 내부에서 특정 상태를 변경하고 다른 상태를 이 변경된 특정 상태의 값으로 변경하면 의도한대로 동작하지 않는다.' 한 이벤트 뿐만 아니라 순차적으로 발생하는 이벤트 체인 내부에서도 동일한 문제가 발생하니 처음 코드를 작성할 때 state 변경 코드를 주의해서 작성하자 정도로 생각하고 넘어갔습니다.
*팀 프로젝트를 진행하면서 이 실수를 정말 많이 보았습니다..

이 후 브라우저 렌더링과 리액트 렌더링을 공부하다 보니 왜 위 처럼 동작하는지 이유를 알게되었습니다. 아래에서 자세히 다뤄보겠습니다.

리액트 렌더링

🔗 https://velog.io/@eunocode/조각조각-리액트렌더링

먼저 알아둘 점은 리액트에서 state가 변경되면 컴포넌트가 검증 과정을 거치게 된다는 것입니다. 컴포넌트 검증 과정에서 이전 가상돔과 변경된 가상돔을 비교해서 수정이 필요한 요소들이 선정되고 실제돔에 적용됩니다. 이는 리렌더링 과정에서 실제돔 조작을 최소화하기 위한 최적화 기법입니다. 여기서 집중해야할 점은 바로 돔조작을 최소화 한다것에 있습니다.

브라우저 렌더링

  1. 파싱
    브라우저는 HTML, CSS, JavaScript 등 웹 페이지 리소스를 서버로부터 가져옵니다.
  2. DOM 트리 생성
    HTML 코드로 DOM 트리가 생성됩니다.
  3. CSSOM 트리 생성
    CSS 코드로 CSSOM 트리가 생성됩니다.
  4. 렌더링 트리 생성
    DOM 트리와 CSSOM 트리가 결합된 렌더링 트리가 생성됩니다. 이때 렌더링 트리에는 페이지를 렌더링하는 데 필요한 노드만 포함됩니다.
  5. 레이아웃
    렌더링 트리를 바탕으로 각 요소의 정확한 위치와 크기를 계산합니다. ( + 상대적인 측정값 -> 절대적인 픽셀 )
  6. 페인팅
    레이아웃 단계에서 계산된 값을 이용해 각 노드를 화면상의 실제 픽셀로 변환하고 화면에 요소들을 그립니다.
  7. 리플로우, 리페인팅
    돔이 조작될 때 렌더 트리와 레이아웃 과정(리플로우)을 다시 수행하고 화면에 그립니다(리페인팅).

브라우저 렌더링 성능 저하 발생

브라우저 렌더링은 위의 과정으로 진행됩니다. 설명되어 있듯이 돔이 조작되면 리플로우, 리페인팅 과정을 거쳐 리렌더링이 발생하게 됩니다. 문제는 이 리플로우 과정과 리페인팅 과정이 적지않은 자원을 사용한다는것 입니다. 그렇기 때문에 돔조작이 빈번하게 발생하는 경우 성능 저하가 발생하게 됩니다.

리액트의 다양한 최적화 기법

위에서 언급햇듯이 리액트는 돔조작을 최소화하는 다양한 최적화 기법이 적용되어있습니다. 이 덕분에 리액트는 빈번한 돔조작이 발생해도 성능 저하를 방지할 수 있습니다. 결론부터 말하자면 이러한 최적화 기법 덕분에 처음 적었던 코드의 결과가 의도한대로 나오지 않게됩니다. 아래에서 자세히 알아보겠습니다.

리액트 최적화 기법 - 가상돔

가상돔 개념을 간단하게 살펴보자면 실제돔을 자바스크립트 객체로 이루어진 가상돔 트리로 복제하고 이 가상돔 트리를 이용해서 이전 가상돔 트리, 현재 가상돔 트리 비교, 변화가 필요한 부분만 찾아내고 실제돔에 적용하여 돔조작을 최적화합니다. 즉 state가 변경되면서 컴포넌트 검증 과정이 진행되고 가상돔 최적화 기법을 이용해 실제돔 조작이 최소화 됩니다. 하지만 이 '가상돔 최적화 기법' 때문에 처음 코드에서 의도치 않은 결과가 나온것이 아닙니다.

리액트 최적화 기법 - setState()의 비동기성

여기서 중요한 점은 state가 변경될때마다 컴포넌트 검증 과정이 진행되는 것이 아니라는 점입니다. 한 이벤트 내부에서 state가 변경 될때마다 컴포넌트 검증과정이 진행되면 리렌더링 즉 돔조작이 빈번히 발생하게 됩니다. 이는 성능저하로 이어집니다. 이것은 리액트 방향성과 매우 멉니다.

그렇기 때문에 리액트는 돔조작을 최소화하기 위해 setState를 비동기적으로 동작시킵니다. 이는 성능 최적화를 위해 여러 setState 호출을 'batch' 처리하여 한 번만 렌더링하도록 하는것입니다. 따라서 여러 번의 setState 호출이 있더라도 16ms 동안 변경된 상태 값을 하나로 묶어 한 번의 컴포넌트 검증 과정을 거치고 리렌더링됩니다.

const [num, setNum] = useState(1);
const [num2, setNum2] = useState(2);

...

const eventA = () => {
    setNum(99);
    setNum2(num);
  
  	//올바르지 않은 결과 확인
  	//console.log(num, num2);
  };

만약 setState가 동기적으로 동작한다면 위 코드에서 num 상태가 99로 변경, 이후 변경된 num 값으로 num2 상태가 변경되어 처음에 의도한 대로 동작했을 것입니다. 하지만 setState는 비동기적으로 동작하기 때문에 이벤트가 동작하면 state num은 99로 state num2는 기존의 num 상태의 값으로 변경되고 'batch' 처리하여 한 번만 렌더링됩니다. 결과적으로 num은 99, num2는 num의 기존값인 1로 변경됩니다.

🚨 여기서 이벤트 내부에서 console.log로 state 변화를 확인하려는 실수를 할 수 있습니다. 이 경우 setState가 비동기적으로 동작해 console.log보다 늦게 처리됩니다. 따라서 state가 변경되기 전 기존값이 출력됩니다.
💡 자바스크립트 이벤트 루프를 공부하면 왜 console.log가 더 늦게 처리되는지 자세히 알 수 있습니다.

정리

  • 리액트 렌더링은 돔조작을 최소화하는 여러 최적화 기법이 적용되어있습니다.
  • 돔조작을 최소화 하기 위해 setState가 비동기적으로 동작합니다.
  • setState가 비동기적으로 동작하기 때문에 setState가 batch 처리되어 한번만 렌더링됩니다. 그렇기 때문에 처음 의도한대로 코드가 동작하지 않게 됩니다.

⭐️ setState가 비동기적으로 동작해 setState 호출이 batch 처리, 한번의 리렌더링만 발생한다!!

참고자료

profile
프론트엔드 개발자

0개의 댓글