결론부터 말하자면
- n번 상태가 변경된다고 할 때 n 회 렌더링을 하지 않기 위해서.
- state 와 props 간의 일관성을 해치지 않기 위해서
-
일단 리액트는 합성(Reconciliation)에 있어서 ‘배치 처리(일괄 처리)’를 기본으로합니다. 즉 , 각각의 업데이트에 대해 모두 합성하는게 아니라 어느정도 합성에서 딜레이를 준 후 한꺼번에 합성을 합니다. 따라서 setState 가 동기적으로 작동한다면 비효율적 입니다.
- why? 브라우저 클릭 이벤트가 발생하고 클릭 이벤트가 발생 할 떄 자식 컴포넌트와 부모 컴포넌트 setState가 실행된다고 생각해봅시다.
- 만약 모든 setState 에 대해 합성을 실행한다고 하면 자식 컴포넌트 setState -> 리렌더링 / 부모 컴포넌트 setState -> 리렌더링 / 부모 컴포넌트의 리렌더링으로 인한 자식컴포넌트 리렌더링 이렇게 자식 컴포넌트에는 2번의 불필요한 리렌더링이 일어나게 됩니다.
-
그러면, state 는 즉각적으로 (동기적으로) 반영하고, 배치처리를 할 수는 없는가를 생각해봅시다.
-
만약 state 가 동기적으로 업데이트 된다고 가정한다고 해도 props 는 그렇지 못합니다.
- 자식 컴포넌트에서는 업데이트 된 props 를 부모 컴포넌트가 리렌더링 될 때 까지 알지 못합니다.
- 만약 props도 즉각적으로 반영된다고 가정하면 ? -> 배치 처리를 포기해야합니다. 위의 예시와 같이 자식컴포넌트에는 결국 불필요한 리렌더링이 추가됩니다.
console.log(this.state.value) // 0
this.setState({ value: this.state.value + 1 });
console.log(this.state.value) // 1
this.setState({ value: this.state.value + 1 });
console.log(this.state.value) // 2
- 위와 같이 setState 가 동기적으로 반영된다고 생각해봅시다.
- 만약에 위의 state 변경을 부모 컴포넌트로 끌어올린다면 어떻게 될까요?
console.log(this.props.value) // 0
this.props.onIncrement();
console.log(this.props.value) // 0
this.props.onIncrement();
console.log(this.props.value) // 0
-
위에서 동기적인 setState 와 다르게 동작합니다.
-
props 는 부모 컴포넌트가 리렌더링 될 때 까지 업데이트 되지 않기 때문입니다.
-
만약 props 역시 즉각적으로 업데이트 되도록 하기 위해서는 배치처리를 포기해야합니다.
-
그러면 리액트는 이러한 문제를 어떻게 해결할까요? 바로 this.state 와 this.props를 합성 이후에 업데이트 합니다
- 지금까지의 이야기로는 리액트는 마치 컴포넌트마다 하나의 업데이트 큐를 가지고 있는 것 같습니다. 우리는 리액트 컴포넌트가 정확히 의도한 순서대로 업데이트 될 것이라고 한치의 의심조차 없었기 때문입니다.
- 하지만, 리액트는 각 setState 호출이 어디에서 호출되는지에 따라서 각기 다른 우선순위를 정할 수 있습니다.
- 예를들어 메시지 창을 구현한다고 했을 때, 사용자가 메시지를 타이핑 할 때 textBox 컴포넌트는 즉각적으로 업데이트 되어야 합니다. 반면, 메시지를 타이핑하는 도중 새로운 메시지가 오면 어떨까요? 당연히 타이핑 중인 textBox 컴포넌트의 업데이트가, 새로운 메시지 컴포넌트 업데이트보다 우선되어야 할 것입니다.
- 이런 식으로 리엑트는 특정 업데이트에 대해 낮은 우선순위를 설정하고, 렌더링을 몇 밀리미초의 작은 chunk 로 분할하여 사용자가 알아채지 못하게 할 수 있습니다.
- 예를 들어 한 화면에서 다른 화면으로 이동 할 경우를 생각해 봅시다. 일반적으로 새 화면을 기다리는 동안 우리는 스피너(로더)를 표시합니다.
- 하지만 화면 전환 속도가 충분히 빠르다면, 아주 찰나의 순간 스피너를 보여준 후 즉시 숨기는 동작은 UX 를 저하 시킬 뿐 아니라 불필요한 DOM 리플로우를 일으켜서 성능에도 좋지 못합니다.
- 따라서 다른 뷰를 렌더링 하는 아주 간단한 setState 를 수행 할 경우, 업데이트 된 뷰를 “백그라운드”에서 렌더링 할 수 있다면 좋지 않을까요?
- 즉, 이를 조정 코드를 직접 작성하지 않고 업데이트가 특정 임계갑(예를들어 1초) 이상 걸리는 경우, 스피너를 보여주고 그렇지 않은 경우 리액트가 화면을 바로 전환한다고 생각해보세요.
- 또한, 기다리는 동안 이전 화면과 여전히 상호작용 가능하고, 리액트가 로딩하는데 시간이 너무 오래걸린다!라고 판단할 경우 스피너를 보여줘야한다고 강제한다면요?
- 즉 이런식의 비동기 렌더링은 성능 뿐만 아니라 사용자 경험을 증진시켜줄 수 있습니다.
-> 이게 현재 리액트에 도입되어있는지는 못찾았음!! 이 글 쓸 당시에 dan abromov 가 뉴 피쳐에 들어갈거라고는 했는데..! 아무튼 비동기 렌더링을 하는 이유! 정도만 알면 좋을 것 같아서 정리해보았습니다
- 결국 이 모든 것은 setState 가 비동기적이기 때문에 가능합니다. 모든 업데이트가 독립적이라면 , 위에서 말했듯 “이전 화면” 이 보이고, 상호작용이 가능한 상태에서 “새로운 화면”을 백그라운드에서 렌더링 할 수 있는 방법이 없습니다.
- 모든 업데이트가 기본적으로 배치 처리 됩니다.
- 서로 다른 이벤트들은 절대로 배치처리 되지 않습니다.
- 예를 들어 서로 다른 버튼에 대한 click 이벤트는 배치처리 되지 않습니다.
react-guide/props-vs-state.md at master · uberVU/react-guide · GitHub
RFClarification: why is setState
asynchronous? · Issue #11527 · Facebook/react · GitHub