자바스크립트의 특성을 이용해 상태 변경을 한번에 처리하자 (싱글 쓰레드)

gun·2024년 2월 29일
0

Mutable 상태 관리의 한계와 해결책

나작프 프레임워크의 상태 관리 방식
나작프 프레임워크에서 사용되는 increment와 decrement 함수를 살펴보면, 두 함수 간에 상태 변경의 방식에 차이가 있음을 알 수 있습니다.

increment() {
    // 2개의 상태를 변경
    this.state.count += 1;
    this.state.text = '리-렌더링';
  }

 decrement() {
    // 1개의 상태를 변경
    this.state.count -= 1;
 }

동일한 작업에 대해 increment()는 2개의 상태를 변경하고, decrement()는 1개의 상태만 변경하기 때문에 동작에 차이가 발생합니다. 따라서 increment()가 호출될 때 리-렌더링이 두 번 발생하는 반면, decrement()는 한 번만 발생하는 것을 확인할 수 있습니다.

이전에 포스팅에서 언급한 것처럼, 나작프는 상태를 mutable하게 관리하고 있었습니다.
자바스크립트의 Proxy를 활용하여 this.state의 변경을 감지하고 중간에서 가로채어 리-렌더링을 할 수 있도록 변경하였습니다.

그러나 이러한 변경으로 인해 발생하는 문제점은 다음과 같습니다. this.state 가 변경되면 즉시 rerender 함수가 실행되기 때문에, this.state 가 동시에 여러 번 변경되면 rerender 함수가 해당 변경 횟수만큼 동작하게 됩니다. 이는 예상치 못한 동작이 발생할 수 있으며, 성능에도 영향을 줄 수 있습니다.

    this.state = new Proxy(this.state, {
      get(target, prop, receiver) {
        return Reflect.get(target, prop, receiver);
      },
      set(target, prop, value, receiver) {
        if (Reflect.set(target, prop, value, receiver)) {
          // component did mount(첫 렌더링) 전에 #_rerender를 할 필요가 없음
          // 성능을 고려 해 한번만 렌더링 되면 됨
          if (rootThis.#isComponentdidMount) {
            rootThis.#_rerender();
          }
          return true;
        }
        // error
        return false;
      }
    });

위 같은 결과로 인해, increment는 한번만 리-렌더링 되어도 될 동작이 2번 리-렌더링 되어 성능을 저하 시키고 있습니다.

문제점과 해결책

React는 해당 이슈를 스케쥴링을 통해 특정 시기에 상태를 업데이트 해주는 방식으로 처리 하고 있습니다.

상태를 변경할 때마다 화면이 다시 렌더링되는 문제를 해결하기 위해, 일정 시간 내에 모든 변경 사항을 적용하고 한 번에 화면을 업데이트하는 방법이 필요 하다고 생각 했습니다.
이를 위해 자바스크립트의 특성을 활용하여 해결책을 이용 할 수 있습니다.

자바스크립트의 특성: 싱글 쓰레드

자바스크립트는 싱글 스레드 언어로서, 한 번에 하나의 작업만 처리할 수 있습니다.

싱글 쓰레드의 특성: 자바스크립트는 한 번에 하나의 작업만 처리합니다. 함수 또는 코드 블록이 실행되면, 해당 작업이 완료될 때까지 다른 작업을 실행하지 않습니다.

Call Stack: 실행 중인 함수나 코드 블록은 호출 스택(Call Stack)에 쌓이게 됩니다. 이 스택은 현재 실행 중인 함수의 정보를 저장하고, 함수가 종료되면 해당 정보를 call stack에서 제거 합니다.

이벤트 루프: 자바스크립트는 비동기적인 작업을 처리하기 위해 이벤트 루프를 사용합니다. 이벤트 루프는 Call Stack이 비어있는지를 주기적으로 확인하고, 비어있으면 이벤트 큐에서 대기 중인 작업을 가져와 실행합니다.

이러한 특성으로 인해 자바스크립트에서는 단일 스레드로 동작하면서도 비동기적인 작업을 처리할 수 있습니다.

setTimeout을 활용한 해결책

가장 간단한 해결책 중 하나는 setTimeout 함수를 사용하는 것입니다. 이 함수를 사용하면 일정 시간이 지난 후에 코드를 실행할 수 있습니다. 이를 통해 상태 변경을 지연시켜 여러 번의 변경이 동시에 발생하는 것을 막을 수 있습니다.
setTimeout 의 일정 시간을 0으로 준다면, 이벤트루프는 callStack을 주시하다가 callStack이 비어있다면, setTimeout의 콜백 함수를 실행 하여, 원하는 결과를 얻을 수 있습니다.

window.requestIdleCallback 활용

더욱 세련된 방법은 window.requestIdleCallback을 활용하는 것입니다. 이 API는 브라우저가 자바스크립트 실행을 마친 뒤에 콜백 함수를 실행하는 방식으로 동작합니다. 자바스크립트의 실행을 마치고, idleTime(유후타임)에 콜백 함수를 실행 시켜줍니다.

이 코드의 실행 순서가 예상 가시나요?

function idleHook() {

    console.log('이벤트 callback')
    requestIdleCallback(() => {
        console.log('DOM 업데이트')
    })
    console.log('Event 끝')
}

결과

// 이벤트 callback
// Event 끝
// DOM 업데이트

requestIdlecallback API는 유후타임에 처리되는 webAPI로 자바스크립트 실행이 끝난 뒤에 실행 되는데, 일정 시간 내에 실행이 완료 되지 않으면 강제로 실행 될 수 있게 되어있어 유후 타임까지 기다릴 필요 없이, 일정 시간에 무조건 실행을 보장 할 수 있습니다.

이러한 실행 조건을 갖고 있는 webAPI를 활용하여 다음과 같은 코드를 작성 했습니다.

  setState(value) {
    if(!this.batchUpdate) {
      this.batchUpdate = true;
      this.pendingState = this.state
     
      if (!this.animationFrameId) {
        this.animationFrameId = requestIdleCallback(() => {
          this.state = this.pendingState
          this.#_rerender();
          this.batchUpdate = false;
          this.animationFrameId = null;
        });
      }
    } 
    this.pendingState = {
      ...this.pendingState,
      ...value
    }
  }

위 코드는 다음과 같은 동작을 하고 있습니다.

  1. setState 사용 시 pendingState의 값 할당
  2. idleCallback API 실행
  3. Event 함수의 실행이 끝나면, dileTIme 일 때 callBack실행
  4. 상태 할당 후 _rerender 함수 실행 화면 다시 그려주기

이러한 동작 과정을 통해 여러 상태가 변경 되어도 한번만 화면을 리-렌더링 해주는 효과를 보여 줄 수 있습니다.

  increment() {
    this.setState({
      count: this.state.count + 1
    })
    this.setState({
      text: '리-렌더'
    })
  }

  decrement() {
    this.setState({
      count: this.state.count - 1
    })
  }

문제 해결

이러한 특성을 이용 해, 데이터는 최종 적으로 1번만 업데이트 되어 화면에 보여줌으로 React와 같이 아래 코드를 작성 해도 count는 1만 증가 해 사이드 이펙트를 방지 할 수 있습니다.

increment() {
    this.setState({
      	count: this.state.count + 1
    })
    this.setState({
		count: this.state.count + 1
    })
  }

마치며

처음 이 문제를 확인하고 다양한 시도를 하며, 최종적으로 자바스크립트의 싱글 쓰레드라는 특징을 사용 해 해결 했습니다. requestIdlecallback API는 실험적인 기능이라 nextjs도 settimeout을 활용 해 사용하고 있는것을 확인 했습니다. ios 환경 에서 사용 시 setTimeout을 활용한 Polyfill을 사용 하시길 바랍니다.

0개의 댓글