1. 리액트의 심장, ‘동등 비교 (Equality Comparison)’
리액트 컴포넌트가 리렌더링되는 가장 대표적인 이유는 props의 변경입니다. 리액트는 “값이 바뀌었는가?”를 판단하기 위해 내부적으로 비교(compare) 과정을 거치며, 이 비교 결과에 따라 렌더링을 수행할지 말지를 결정합니다.
이 비교 방식을 이해하지 못하면 다음과 같은 문제를 쉽게 겪게 됩니다.
useEffect가 의도치 않게 반복 실행되거나React.memo를 사용했는데도 불필요한 렌더링이 발생하거나useMemo,useCallback의 필요성을 체감하지 못하는 상황
사실 리액트의 가상 DOM 비교, 의존성 배열 비교, 메모이제이션 로직의 근간에는 자바스크립트의 데이터 타입과 동등 비교 규칙이 자리 잡고 있습니다. 따라서 리액트의 렌더링 원리를 이해하려면, 먼저 자바스크립트의 비교 방식부터 짚고 넘어가야 합니다.
1.1 자바스크립트의 데이터 타입과 비교의 한계
리액트의 비교 방식을 이해하기 위해서는, 자바스크립트가 값을 어떻게 저장하고 비교하는지부터 살펴볼 필요가 있습니다.
자바스크립트의 데이터 타입은 크게 다음 두 가지로 나뉩니다.
원시 타입 (Primitive Type)
boolean,null,undefined,number,string,symbol,bigint- 값(Value) 자체가 불변(immutable) 형태로 저장됨
객체 타입 (Object / Reference Type)
- 객체, 배열, 함수 등
- 값이 아닌 참조(Reference) 형태로 저장됨
이 차이로 인해 비교 결과에서 중요한 차이가 발생합니다.
(1) 원시타입의 비교
원시 타입은 값 자체를 복사하여 전달합니다. 따라서 서로 다른 변수라도 값이 같다면 비교 결과는 true입니다.
let hello = 'hello world'
let hi = 'hello world'
console.log(hello === hi) // true
자바스크립트 엔진은 값을 그대로 비교하기 때문에, 내용이 같다면 동일하다고 판단합니다.
(2) 객체 타입의 비교
객체 타입은 값이 아니라 메모리 주소(참조)를 복사하여 전달합니다. 따라서 내부 프로퍼티 값이 완전히 동일하더라도, 참조가 다르면 false를 반환합니다.
var hello = { greet: 'hello, world' }
var hi = { greet: 'hello, world' }
console.log(hello === hi) // false
이처럼 자바스크립트에서 객체 비교는 “내용이 같은가?”가 아니라 “같은 객체인가?”를 기준으로 이루어집니다.
이 특성은 리액트의 렌더링 동작에 직접적인 영향을 미치게 됩니다.
1.2 리액트만의 비교 공식: Object.is와 얕은 비교 (shallowEqual)
리액트는 렌더링 최적화를 위해 단순히 자바스크립트의 === 연산자만 사용하지 않습니다.
대신 ES6에서 도입된 Object.is를 기반으로 한 비교 방식을 채택하고 있습니다.
(1) Object.is의 도입
=== 연산자는 대부분의 경우 정확하지만, 다음과 같은 예외가 존재합니다.
-0 === +0 // true
NaN === NaN // false
리액트는 이러한 예외까지 정확하게 처리하기 위해 Object.is 알고리즘을 사용합니다.
Object.is(-0, +0) // false
Object.is(NaN, NaN) // true
이를 통해 리액트는 보다 일관된 비교 결과를 얻을 수 있습니다.
(2) 리액트의 얕은 비교 (shallowEqual)
리액트는 Object.is를 기반으로 shallowEqual(얕은 비교)라는 함수를 구현하여 사용합니다.
이 비교 방식은 다음과 같은 규칙을 따릅니다.
- 먼저 두 값을
Object.is로 비교 - 값이 다르고 객체라면, 첫 번째 깊이(depth)의 프로퍼티만 비교
- 중첩된 객체 내부까지는 비교하지 않음
왜 얕은 비교까지만 할까?
리액트에서 사용하는 props는 기본적으로 객체입니다. 리액트는 props 객체 안에 들어 있는 값들이 변경되었는지를 기준으로 렌더링 여부를 판단하므로, 대부분의 경우 1 depth의 얕은 비교만으로 충분합니다.
만약 리액트가 깊은 비교(Deep Compare)를 수행한다면,
- 객체 안에 객체가 중첩된 구조를
- 렌더링마다 재귀적으로 탐색해야 하고
- 이는 곧 심각한 성능 저하로 이어지게 됩니다.
그래서 리액트는 비교 비용을 최소화하는 대신, 개발자가 참조 변경을 통해 변경 의도를 표현하도록 설계되었습니다.