React

[Nextjs]트러블슈팅: 브라우저의 프로세스와 스레드, 과부하로 인한 이벤트 생략?

변기원 2024. 7. 8. 23:14

이 글을 리액트 코드의 성능과 브라우저에 대한 내용이다.

분명히 발생해야 할 이벤트가 무시될 수도 있을까?

아래와 같은 코드가 있었다

const [barcode, setBarcode] = useState('');

const handleBarcode = (e) => {
  setBarcode(e.target.value);
};

const handleEnter = async () => {
  // await post('/barcode',{barcode})
};

return (
  <>
    <input onChange={handleBarcode} onKeyDown={handleEnter} />
    <Table {...props} />
  </>
);

 

barcode 값은 바코드리더기가 읽어낸 QR코드의 value값이다. 

예를 들어 "eij1oidj012d93j980dj128d12987" 이렇게 생긴 스트링이 바코드 value값으로 읽힌다고 보면 된다.

input에 포커스를 두고 QR코드를 스캔하면 위와 같은 스트링이 들어오고 마지막에는 enter키 이벤트가 발생해서 onKeyDown이벤트에서 엔터키 발생 시 백엔드 api를 호출하는 코드를 작성하면 된다.

 

문제는 여기서 발생했다.

분명히 50글자의 string이 입력되었는데 api에 넘긴 페이로드는 47글자밖에 안 된다!

화면에 보이는 input에는 분명히 50글자가 입력되어 있다.

이벤트가 전부 발생하지 않아서 setBarcode를 세 번 빼먹은 것이다.

 

같은 로직이 있는 다른 페이지에서는 정상적으로 동작해서 원인파악까지 애먹었다.

이벤트는 50글자가 한 번에 입력되는 게 아니라 1,2,3,4,5,6,..... 50 자릿수까지 전부 50회 연달아 발생했다. 

그렇다면 setState가 50번 연달아 발생할 것이고, 해당 컴포넌트의 state가 변경되면서 자식컴포넌트를 리랜더링 한다.

Table컴포넌트만 지우면 정상적으로 작동했는데, 바로 Table컴포넌트에 Dom이 많았기 때문에 리랜더링시 부하가 발생하는 것이다.

게다가 0.1초도 안되어서 50번의 이벤트가 연달아 발생했다. 분명히 어딘가에 과부하가 발생할 것이라고 생각했고,

Table을 메모이제이션 하거나 barcode input을 비제어 컴포넌트로 만들어서 이슈를 해결했다.

 

하지만 이슈 해결과 별개로 과부하가 발생하는 어딘가 가 어딜까? 가 너무 궁금했다.

https://d2.naver.com/helloworld/9274593

네이버 D2블로그에서 힌트를 찾을 수 있었다.

엄청나게 내용이 방대하지만 위 이슈에 초점을 맞춰서 요약하자면..

 

(크롬)브라우저는 사이트마다 프로세스를 할당한다. 탭이 아니라 사이드마다이다. 그래서 iframe도 하나의 프로세스를 할당받는다.

그 이유는 프로세스마다 메모리공간이 주어지는데 탭마다 프로세스를 부여한다면, iframe으로 띄운 다른 사이트에서도 같은 메모리를 할당받게 된다. 이것이 보안 취약점이 된다고 한다. 

그래서 내 웹앱은 하나의 프로세스를 할당받고 이것이 바로 렌더링 프로세스이다.

브라우저 프로세스와 달리 렌더링 프로세스는 탭, 뒤로 가기 영역 같은 브라우저 고유의 영역 외에 흔히 우리가 화면이라고 부르는 그 부분에 대한 모든 작업을 책임진다.

렌더링 프로세스는 하나의 메인스레드를 가지고 있는데 이것이 바로 자바스크립트 엔진이 작동하는 메인 스레드이다.

'자바스크립트는 싱글스레드 언어이다'의 바로 그 싱글스레드가 렌더링 프로세스의 메인스레드이다.

아무튼 이 메인스레드는 엄청 많은 일을 하는데 대표적인 게 바로 DOM 렌더링, 자바스크립트 실행, 이벤트 핸들링, 레이아웃 계산이다. 엄청 중요한 일을 전부 이 메인스레드가 하는데, 일반적으로 연속적인 이벤트(touchmove, mousewheel 같은 것들...)가 보통 1초에 120번 정도 호출된다고 본다. 반면에 화면은 1초에 60번 새로 갱신하는데, 연속적인 이벤트의 경우 화면 갱신보다 이벤트 발생이 많아질 수 있기 때문에 1초를 60번으로 나눠서 0.017초 안에 여러 번 발생한 이벤트는 합쳐서 대기한 뒤에 requestAnimationFrame() 메서드 실행까지 대기한다. 

하지만 이번 케이스는 더 심각한데, 일반적이지 않기 때문이다, onChange이벤트는 연속적인 이벤트로 분류되지 않기 때문에 합치거나 대기하지 않고 모두 발생시키는 것을 원칙으로 한다. 하지만 0.1초도 발생한 50번의 change이벤트, 그리고 그때마다 리랜더링 되는 Table에 포함된 수많은 DOM element의 리랜더링까지 모두 메인스레드의 몫이다. 이러한 이유로 메인스레드에 과부하가 발생했고, 50번의 이벤트를 모두 발생시키지 못하게 되었다.

 

정확히 어떤 이유로 이러한 이슈가 발생했는지 체크하고 넘어가면 다음부터는 이슈 파악이 빨라진다.