본문 바로가기

글/나의 생각, 나의 삶

크루들과 나눈 우아한 대화 #1 (React rendering, commit phase, memo)

크루들과 나눈 우아한 대화 #1

 

아놀드, 병민, 앨버와 아주 재밌는 대화를 나누었고, 많이 배웠다.

 

대화의 시작은 이러했다.

 

"Props로 item 정보가 담긴 객체와 아이템을 삭제하는 함수 (deleteItem)을 넘겨준다고 했을 때, 

useCallback과 memo로 초기화 하는 것이 의미가 있을까?"

 

return (
	<>
	  itemList.map(item => <Item item={item} onClick={deleteItem} />)
	</>
)

 

이 상황에서 하나의 item이 업데이트 되면, 모든 Item이 리렌더링 될 것이다.

내가 가진 의문은, 연관없는 Item 컴포넌트가 다시 그려질지언정, Item이 같은 것을 그리고 있다면 diff 알고리즘이 바뀌었다고 인식하지 않기 때문에, 리렌더링 하지 않을까? 하는 것이었다.

 

내가 잘못 알거나 / 얕게 알고있었던 것은

- 렌더링이란 (함수) 컴포넌트를 실행하는 것을 의미하는가? 변화를 파악하고 다시 그리는 것을 의미하는가?

- diffing 알고리즘이 언제 다시 그리는가?

 

우리가 나눴던 대화

 

- 리액트에는 render phase와 commit phase가 있다.

- render phase란, 함수를(함수형 컴포넌트의 경우) 실행하는 하고, 함수의 결과값(JSX === 객체 === virtual DOM node)과 이전 virtual DOM node를 비교하는 과정을 통해, diff 가 있다면, 다시 그리라고 체크하라고 하는 것까지의 과정을 의미한다.

- commit phase란 diff 를 real DOM에 반영하는 과정을 의미한다.

- render phase에서 diff 결과, 변화가 있을 때, 다시 그리는 DOM을 선택하는 것을 dirty check라고 부른다.

- dirty check는 병민이 만든 말인가? 라는 논란이 있었다. ㅋㅋ 지나가는 공원에게 "공원, dirty check 들어보셨나요?" 하고 물었다. 공원이 dirty check에 대해 설명해주셨고, 병민이 만든 단어라는 논란은 종결되었고, 병민을 신뢰하였다.

 

- 위 코드에서 useCallback과 memo를 사용하는 것은 무의미하냐? 

- 그렇지 않다. diffing 알고리즘은, 엘리먼트의 type(tag name), key, props를 비교한다. Item이 동일한 값을 유지하고 있을 지언정, 다른 참조값을 가진 function이 부착되므로 (즉, 다른 props이므로) diffing 알고리즘은 dirty check하고, commit phase를 실행한다.

- useCallback과 memo를 사용한다면, 다른 함수 참조값이 들어오므로, dirty check하지 않고, commit phase로 들어가지 않는다.

 

- 그렇다면 useCallback 을 사용하고, memo를 쓰지 않는 것은 무의미하냐? 하는 의문이 있었다.

- useCallback은 참조동일성을 위해 존재한다. memo 또한 참조 동일성이다. 이때 dependency array는 props 였다. 결국, memo는 useMemo와 다르지 않다는 것이다. 

- useCallback으로 참조 동일성을 유지해주고, 하위 컴포넌트에 props로 내려주고, 컴포넌트에 함수를 부착했다고 치자.

- 이때 deps가 변하지 않는 한, 리렌더링이 발생하지 않을 것이다. 

- deps에 포함된 state 이외의 state가 변화했다면, 하위 컴포넌트를 렌더링 할 것이다.(함수를 실행할 것이다.)

- 함수가 참조동일성을 유지하고 있기 때문에, 하위 컴포넌트에도 변화가 없을 것이고, dirty check된 녀석이 없기 때문에, commit phase로 넘어가지는 않을 것이다.

- 즉, 렌더링은 한다는 것. 그리고 memoization된 함수를 commit phase로 넣지 않는다는 효과가 있을 것이다.

- 여기에 memo를 더해준다면 렌더링까지 막을 수 있다.

 

- memo가 렌더링을 막아주는 것이냐? 하는 의문이 있었다.

- 좀 더 엄밀히 말해야한다. 일단 "부모 컴포넌트가 dirty check되면, 하위 컴포넌트 모두를 commit phase 한다"

- 그러면 memo는 commit phase를 막아버리는 것이냐?

- useMemo와 동일하다. 코드를 실행하다가 useMemo를 만나면, 함수를 선언하긴 하지만, deps가 변하지 않으면, 함수값을 memory cells에서 꺼내온다.

- memo도 함수(컴포넌트)를 선언하긴 하지만, deps(props)가 변하지 않으면, memory cells에서 함수값(virtual DOM node === 객체)를 꺼내온다.

- 즉, 렌더링을 막는다기보다, memory cells에서 JSX를 꺼내온다고 생각하자

 

오늘 느낀 점

- 병민/앨버는 리액트를 진짜 깊게 공부한다. 리액트를 깊게 알다보니, 언제 어떤 훅을 써야 될지 잘 판단하는 것 같다.

- 아놀드는 자바스크립트를 진짜 깊게 공부한다. 아놀드는 자바스크립트를 깊게 공부하다보니, useMemo, useState, useCallback의 구현체를 직접 떠올릴 수 있는 수준이다. 리액트를 일주일 공부하고 아래 코드를 스스로 생각할 수 있었다는 게 너무 놀라웠다.

 

최근에 개발 관련 고민이 매우 많아졌다. 하지만 공교롭게도 해결하고 있는 것이 거의 없다. 내가 하고 있는 고민은 대부분 docs에 나오지 않는다. 이제 docs는 어느정도 이해했고, 이 이상을 넘어야하는 단계에 놓였다고 생각한다. docs 이상의 레벨은 검색으로 찾기 어려운 경우가 많다. 코드를 뜯어보거나, 좀 더 근본적인 원리를 이해하며 스스로 생각해야하는 영역이다. 코드를 뜯어볼 힘이 부족하고, 근본적인 원리를 잘 이해하고 있지 않다.

 

핵심을 모르면, 하나 하나의 부수적인 의문을 아무리 열심히 고민한다고 한들 알 수 없다. "함수 실행과 렌더링은 같은 말인가?" "새로 그리는 것을 렌더링 이라고 하는가?" "useState는 어떻게 구현되었는가? Closure로?" "memory cells에 어떻게 저장하고 있을까?" 찾아보기 어려운 의문들이었다. 이럴 때일 수록 기초를 다져서, 혼자 고민할 수 있는 힘을 길러야 한다.

 

결국은 자바스크립트를 공부해야 하고, rendering 과정, hook의 참조 기억 방식, diffing 알고리즘, render/commit phase, react scheduler 등 근본적인 내용을 공부해야 한다.